require "socket"
require "monitor"
require "digest/md5"
begin
require "openssl"
rescue LoadError
end
module Net
class IMAP
include MonitorMixin
if defined?(OpenSSL)
include OpenSSL
include SSL
end
attr_reader :greeting
attr_reader :responses
attr_reader :response_handlers
attr_accessor :client_thread
SEEN = :Seen
ANSWERED = :Answered
FLAGGED = :Flagged
DELETED = :Deleted
DRAFT = :Draft
RECENT = :Recent
NOINFERIORS = :Noinferiors
NOSELECT = :Noselect
MARKED = :Marked
UNMARKED = :Unmarked
def self.debug
return @@debug
end
def self.debug=(val)
return @@debug = val
end
def self.add_authenticator(auth_type, authenticator)
@@authenticators[auth_type] = authenticator
end
def disconnect
begin
@sock.io.shutdown
rescue NoMethodError
@sock.shutdown
end
@receiver_thread.join
@sock.close
end
def disconnected?
return @sock.closed?
end
def capability
synchronize do
send_command("CAPABILITY")
return @responses.delete("CAPABILITY")[-1]
end
end
def noop
send_command("NOOP")
end
def logout
send_command("LOGOUT")
end
def authenticate(auth_type, *args)
auth_type = auth_type.upcase
unless @@authenticators.has_key?(auth_type)
raise ArgumentError,
format('unknown auth type - "%s"', auth_type)
end
authenticator = @@authenticators[auth_type].new(*args)
send_command("AUTHENTICATE", auth_type) do |resp|
if resp.instance_of?(ContinuationRequest)
data = authenticator.process(resp.data.text.unpack("m")[0])
s = [data].pack("m").gsub(/\n/, "")
send_string_data(s)
put_string(CRLF)
end
end
end
def login(user, password)
send_command("LOGIN", user, password)
end
def select(mailbox)
synchronize do
@responses.clear
send_command("SELECT", mailbox)
end
end
def examine(mailbox)
synchronize do
@responses.clear
send_command("EXAMINE", mailbox)
end
end
def create(mailbox)
send_command("CREATE", mailbox)
end
def delete(mailbox)
send_command("DELETE", mailbox)
end
def rename(mailbox, newname)
send_command("RENAME", mailbox, newname)
end
def subscribe(mailbox)
send_command("SUBSCRIBE", mailbox)
end
def unsubscribe(mailbox)
send_command("UNSUBSCRIBE", mailbox)
end
def list(refname, mailbox)
synchronize do
send_command("LIST", refname, mailbox)
return @responses.delete("LIST")
end
end
def getquotaroot(mailbox)
synchronize do
send_command("GETQUOTAROOT", mailbox)
result = []
result.concat(@responses.delete("QUOTAROOT"))
result.concat(@responses.delete("QUOTA"))
return result
end
end
def getquota(mailbox)
synchronize do
send_command("GETQUOTA", mailbox)
return @responses.delete("QUOTA")
end
end
def setquota(mailbox, quota)
if quota.nil?
data = '()'
else
data = '(STORAGE ' + quota.to_s + ')'
end
send_command("SETQUOTA", mailbox, RawData.new(data))
end
def setacl(mailbox, user, rights)
if rights.nil?
send_command("SETACL", mailbox, user, "")
else
send_command("SETACL", mailbox, user, rights)
end
end
def getacl(mailbox)
synchronize do
send_command("GETACL", mailbox)
return @responses.delete("ACL")[-1]
end
end
def lsub(refname, mailbox)
synchronize do
send_command("LSUB", refname, mailbox)
return @responses.delete("LSUB")
end
end
def status(mailbox, attr)
synchronize do
send_command("STATUS", mailbox, attr)
return @responses.delete("STATUS")[-1].attr
end
end
def append(mailbox, message, flags = nil, date_time = nil)
args = []
if flags
args.push(flags)
end
args.push(date_time) if date_time
args.push(Literal.new(message))
send_command("APPEND", mailbox, *args)
end
def check
send_command("CHECK")
end
def close
send_command("CLOSE")
end
def expunge
synchronize do
send_command("EXPUNGE")
return @responses.delete("EXPUNGE")
end
end
def search(keys, charset = nil)
return search_internal("SEARCH", keys, charset)
end
def uid_search(keys, charset = nil)
return search_internal("UID SEARCH", keys, charset)
end
def fetch(set, attr)
return fetch_internal("FETCH", set, attr)
end
def uid_fetch(set, attr)
return fetch_internal("UID FETCH", set, attr)
end
def store(set, attr, flags)
return store_internal("STORE", set, attr, flags)
end
def uid_store(set, attr, flags)
return store_internal("UID STORE", set, attr, flags)
end
def copy(set, mailbox)
copy_internal("COPY", set, mailbox)
end
def uid_copy(set, mailbox)
copy_internal("UID COPY", set, mailbox)
end
def sort(sort_keys, search_keys, charset)
return sort_internal("SORT", sort_keys, search_keys, charset)
end
def uid_sort(sort_keys, search_keys, charset)
return sort_internal("UID SORT", sort_keys, search_keys, charset)
end
def add_response_handler(handler = Proc.new)
@response_handlers.push(handler)
end
def remove_response_handler(handler)
@response_handlers.delete(handler)
end
def thread(algorithm, search_keys, charset)
return thread_internal("THREAD", algorithm, search_keys, charset)
end
def uid_thread(algorithm, search_keys, charset)
return thread_internal("UID THREAD", algorithm, search_keys, charset)
end
def self.decode_utf7(s)
return s.gsub(/&(.*?)-/n) {
if $1.empty?
"&"
else
base64 = $1.tr(",", "/")
x = base64.length % 4
if x > 0
base64.concat("=" * (4 - x))
end
u16tou8(base64.unpack("m")[0])
end
}
end
def self.encode_utf7(s)
return s.gsub(/(&)|([^\x20-\x7e]+)/u) { |x|
if $1
"&-"
else
base64 = [u8tou16(x)].pack("m")
"&" + base64.delete("=\n").tr("/", ",") + "-"
end
}
end
private
CRLF = "\r\n" PORT = 143
@@debug = false
@@authenticators = {}
def initialize(host, port = PORT, usessl = false, certs = nil, verify = false)
super()
@host = host
@port = port
@tag_prefix = "RUBY"
@tagno = 0
@parser = ResponseParser.new
@sock = TCPSocket.open(host, port)
if usessl
unless defined?(OpenSSL)
raise "SSL extension not installed"
end
@usessl = true
context = SSLContext::new()
context.ca_file = certs if certs && FileTest::file?(certs)
context.ca_path = certs if certs && FileTest::directory?(certs)
context.verify_mode = VERIFY_PEER if verify
if defined?(VerifyCallbackProc)
context.verify_callback = VerifyCallbackProc
end
@sock = SSLSocket.new(@sock, context)
@sock.sync_close = true
@sock.connect @sock.post_connection_check(@host) if verify
else
@usessl = false
end
@responses = Hash.new([].freeze)
@tagged_responses = {}
@response_handlers = []
@response_arrival = new_cond
@continuation_request = nil
@logout_command_tag = nil
@debug_output_bol = true
@exception = nil
@greeting = get_response
if @greeting.name == "BYE"
@sock.close
raise ByeResponseError, @greeting.raw_data
end
@client_thread = Thread.current
@receiver_thread = Thread.start {
receive_responses
}
end
def receive_responses
while true
synchronize do
@exception = nil
end
begin
resp = get_response
rescue Exception => e
synchronize do
@sock.close unless @sock.closed?
@exception = e
end
break
end
unless resp
synchronize do
@exception = EOFError.new("end of file reached")
end
break
end
begin
synchronize do
case resp
when TaggedResponse
@tagged_responses[resp.tag] = resp
@response_arrival.broadcast
if resp.tag == @logout_command_tag
return
end
when UntaggedResponse
record_response(resp.name, resp.data)
if resp.data.instance_of?(ResponseText) &&
(code = resp.data.code)
record_response(code.name, code.data)
end
if resp.name == "BYE" && @logout_command_tag.nil?
@sock.close
@exception = ByeResponseError.new(resp.raw_data)
@response_arrival.broadcast
return
end
when ContinuationRequest
@continuation_request = resp
@response_arrival.broadcast
end
@response_handlers.each do |handler|
handler.call(resp)
end
end
rescue Exception => e
@exception = e
synchronize do
@response_arrival.broadcast
end
end
end
synchronize do
@response_arrival.broadcast
end
end
def get_tagged_response(tag)
until @tagged_responses.key?(tag)
raise @exception if @exception
@response_arrival.wait
end
return pick_up_tagged_response(tag)
end
def pick_up_tagged_response(tag)
resp = @tagged_responses.delete(tag)
case resp.name
when /\A(?:NO)\z/ni
raise NoResponseError, resp.data.text
when /\A(?:BAD)\z/ni
raise BadResponseError, resp.data.text
else
return resp
end
end
def get_response
buff = ""
while true
s = @sock.gets(CRLF)
break unless s
buff.concat(s)
if /\{(\d+)\}\r\n/n =~ s
s = @sock.read($1.to_i)
buff.concat(s)
else
break
end
end
return nil if buff.length == 0
if @@debug
$stderr.print(buff.gsub(/^/n, "S: "))
end
return @parser.parse(buff)
end
def record_response(name, data)
unless @responses.has_key?(name)
@responses[name] = []
end
@responses[name].push(data)
end
def send_command(cmd, *args, &block)
synchronize do
tag = Thread.current[:net_imap_tag] = generate_tag
put_string(tag + " " + cmd)
args.each do |i|
put_string(" ")
send_data(i)
end
put_string(CRLF)
if cmd == "LOGOUT"
@logout_command_tag = tag
end
if block
add_response_handler(block)
end
begin
return get_tagged_response(tag)
ensure
if block
remove_response_handler(block)
end
end
end
end
def generate_tag
@tagno += 1
return format("%s%04d", @tag_prefix, @tagno)
end
def put_string(str)
@sock.print(str)
if @@debug
if @debug_output_bol
$stderr.print("C: ")
end
$stderr.print(str.gsub(/\n(?!\z)/n, "\nC: "))
if /\r\n\z/n.match(str)
@debug_output_bol = true
else
@debug_output_bol = false
end
end
end
def send_data(data)
case data
when nil
put_string("NIL")
when String
send_string_data(data)
when Integer
send_number_data(data)
when Array
send_list_data(data)
when Time
send_time_data(data)
when Symbol
send_symbol_data(data)
else
data.send_data(self)
end
end
def send_string_data(str)
case str
when ""
put_string('""')
when /[\x80-\xff\r\n]/n
send_literal(str)
when /[(){ \x00-\x1f\x7f%*"\\]/n
# quoted string
send_quoted_string(str)
else
put_string(str)
end
end
def send_quoted_string(str)
put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
end
def send_literal(str)
put_string("{" + str.length.to_s + "}" + CRLF)
while @continuation_request.nil? &&
!@tagged_responses.key?(Thread.current[:net_imap_tag])
@response_arrival.wait
raise @exception if @exception
end
if @continuation_request.nil?
pick_up_tagged_response(Thread.current[:net_imap_tag])
raise ResponseError.new("expected continuation request")
end
@continuation_request = nil
put_string(str)
end
def send_number_data(num)
if num < 0 || num >= 4294967296
raise DataFormatError, num.to_s
end
put_string(num.to_s)
end
def send_list_data(list)
put_string("(")
first = true
list.each do |i|
if first
first = false
else
put_string(" ")
end
send_data(i)
end
put_string(")")
end
DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
def send_time_data(time)
t = time.dup.gmtime
s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
t.day, DATE_MONTH[t.month - 1], t.year,
t.hour, t.min, t.sec)
put_string(s)
end
def send_symbol_data(symbol)
put_string("\\" + symbol.to_s)
end
def search_internal(cmd, keys, charset)
if keys.instance_of?(String)
keys = [RawData.new(keys)]
else
normalize_searching_criteria(keys)
end
synchronize do
if charset
send_command(cmd, "CHARSET", charset, *keys)
else
send_command(cmd, *keys)
end
return @responses.delete("SEARCH")[-1]
end
end
def fetch_internal(cmd, set, attr)
case attr
when String then
attr = RawData.new(attr)
when Array then
attr = attr.map { |arg|
arg.is_a?(String) ? RawData.new(arg) : arg
}
end
synchronize do
@responses.delete("FETCH")
send_command(cmd, MessageSet.new(set), attr)
return @responses.delete("FETCH")
end
end
def store_internal(cmd, set, attr, flags)
if attr.instance_of?(String)
attr = RawData.new(attr)
end
synchronize do
@responses.delete("FETCH")
send_command(cmd, MessageSet.new(set), attr, flags)
return @responses.delete("FETCH")
end
end
def copy_internal(cmd, set, mailbox)
send_command(cmd, MessageSet.new(set), mailbox)
end
def sort_internal(cmd, sort_keys, search_keys, charset)
if search_keys.instance_of?(String)
search_keys = [RawData.new(search_keys)]
else
normalize_searching_criteria(search_keys)
end
normalize_searching_criteria(search_keys)
synchronize do
send_command(cmd, sort_keys, charset, *search_keys)
return @responses.delete("SORT")[-1]
end
end
def thread_internal(cmd, algorithm, search_keys, charset)
if search_keys.instance_of?(String)
search_keys = [RawData.new(search_keys)]
else
normalize_searching_criteria(search_keys)
end
normalize_searching_criteria(search_keys)
send_command(cmd, algorithm, charset, *search_keys)
return @responses.delete("THREAD")[-1]
end
def normalize_searching_criteria(keys)
keys.collect! do |i|
case i
when -1, Range, Array
MessageSet.new(i)
else
i
end
end
end
def self.u16tou8(s)
len = s.length
if len < 2
return ""
end
buf = ""
i = 0
while i < len
c = s[i] << 8 | s[i + 1]
i += 2
if c == 0xfeff
next
elsif c < 0x0080
buf.concat(c)
elsif c < 0x0800
b2 = c & 0x003f
b1 = c >> 6
buf.concat(b1 | 0xc0)
buf.concat(b2 | 0x80)
elsif c >= 0xdc00 && c < 0xe000
raise DataFormatError, "invalid surrogate detected"
elsif c >= 0xd800 && c < 0xdc00
if i + 2 > len
raise DataFormatError, "invalid surrogate detected"
end
low = s[i] << 8 | s[i + 1]
i += 2
if low < 0xdc00 || low > 0xdfff
raise DataFormatError, "invalid surrogate detected"
end
c = (((c & 0x03ff)) << 10 | (low & 0x03ff)) + 0x10000
b4 = c & 0x003f
b3 = (c >> 6) & 0x003f
b2 = (c >> 12) & 0x003f
b1 = c >> 18;
buf.concat(b1 | 0xf0)
buf.concat(b2 | 0x80)
buf.concat(b3 | 0x80)
buf.concat(b4 | 0x80)
else b3 = c & 0x003f
b2 = (c >> 6) & 0x003f
b1 = c >> 12
buf.concat(b1 | 0xe0)
buf.concat(b2 | 0x80)
buf.concat(b3 | 0x80)
end
end
return buf
end
private_class_method :u16tou8
def self.u8tou16(s)
len = s.length
buf = ""
i = 0
while i < len
c = s[i]
if (c & 0x80) == 0
buf.concat(0x00)
buf.concat(c)
i += 1
elsif (c & 0xe0) == 0xc0 &&
len >= 2 &&
(s[i + 1] & 0xc0) == 0x80
if c == 0xc0 || c == 0xc1
raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
end
u = ((c & 0x1f) << 6) | (s[i + 1] & 0x3f)
buf.concat(u >> 8)
buf.concat(u & 0x00ff)
i += 2
elsif (c & 0xf0) == 0xe0 &&
i + 2 < len &&
(s[i + 1] & 0xc0) == 0x80 &&
(s[i + 2] & 0xc0) == 0x80
if c == 0xe0 && s[i + 1] < 0xa0
raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
end
u = ((c & 0x0f) << 12) | ((s[i + 1] & 0x3f) << 6) | (s[i + 2] & 0x3f)
if u >= 0xd800 && u <= 0xdfff
raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
end
buf.concat(u >> 8)
buf.concat(u & 0x00ff)
i += 3
elsif (c & 0xf8) == 0xf0 &&
i + 3 < len &&
(s[i + 1] & 0xc0) == 0x80 &&
(s[i + 2] & 0xc0) == 0x80 &&
(s[i + 3] & 0xc0) == 0x80
if c == 0xf0 && s[i + 1] < 0x90
raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
end
u = ((c & 0x07) << 18) | ((s[i + 1] & 0x3f) << 12) |
((s[i + 2] & 0x3f) << 6) | (s[i + 3] & 0x3f)
if u < 0x10000
buf.concat(u >> 8)
buf.concat(u & 0x00ff)
elsif u < 0x110000
high = ((u - 0x10000) >> 10) | 0xd800
low = (u & 0x03ff) | 0xdc00
buf.concat(high >> 8)
buf.concat(high & 0x00ff)
buf.concat(low >> 8)
buf.concat(low & 0x00ff)
else
raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
end
i += 4
else
raise DataFormatError, format("illegal UTF-8 sequence (%02x)", c)
end
end
return buf
end
private_class_method :u8tou16
class RawData def send_data(imap)
imap.send(:put_string, @data)
end
private
def initialize(data)
@data = data
end
end
class Atom def send_data(imap)
imap.send(:put_string, @data)
end
private
def initialize(data)
@data = data
end
end
class QuotedString def send_data(imap)
imap.send(:send_quoted_string, @data)
end
private
def initialize(data)
@data = data
end
end
class Literal def send_data(imap)
imap.send(:send_literal, @data)
end
private
def initialize(data)
@data = data
end
end
class MessageSet def send_data(imap)
imap.send(:put_string, format_internal(@data))
end
private
def initialize(data)
@data = data
end
def format_internal(data)
case data
when "*"
return data
when Integer
ensure_nz_number(data)
if data == -1
return "*"
else
return data.to_s
end
when Range
return format_internal(data.first) +
":" + format_internal(data.last)
when Array
return data.collect {|i| format_internal(i)}.join(",")
when ThreadMember
return data.seqno.to_s +
":" + data.children.collect {|i| format_internal(i).join(",")}
else
raise DataFormatError, data.inspect
end
end
def ensure_nz_number(num)
if num < -1 || num == 0 || num >= 4294967296
msg = "nz_number must be non-zero unsigned 32-bit integer: " +
num.inspect
raise DataFormatError, msg
end
end
end
ContinuationRequest = Struct.new(:data, :raw_data)
UntaggedResponse = Struct.new(:name, :data, :raw_data)
TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
ResponseText = Struct.new(:code, :text)
ResponseCode = Struct.new(:name, :data)
MailboxList = Struct.new(:attr, :delim, :name)
MailboxQuota = Struct.new(:mailbox, :usage, :quota)
MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
MailboxACLItem = Struct.new(:user, :rights)
StatusData = Struct.new(:mailbox, :attr)
FetchData = Struct.new(:seqno, :attr)
Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
:to, :cc, :bcc, :in_reply_to, :message_id)
Address = Struct.new(:name, :route, :mailbox, :host)
ContentDisposition = Struct.new(:dsp_type, :param)
ThreadMember = Struct.new(:seqno, :children)
class BodyTypeBasic < Struct.new(:media_type, :subtype,
:param, :content_id,
:description, :encoding, :size,
:md5, :disposition, :language,
:extension)
def multipart?
return false
end
def media_subtype
$stderr.printf("warning: media_subtype is obsolete.\n")
$stderr.printf(" use subtype instead.\n")
return subtype
end
end
class BodyTypeText < Struct.new(:media_type, :subtype,
:param, :content_id,
:description, :encoding, :size,
:lines,
:md5, :disposition, :language,
:extension)
def multipart?
return false
end
def media_subtype
$stderr.printf("warning: media_subtype is obsolete.\n")
$stderr.printf(" use subtype instead.\n")
return subtype
end
end
class BodyTypeMessage < Struct.new(:media_type, :subtype,
:param, :content_id,
:description, :encoding, :size,
:envelope, :body, :lines,
:md5, :disposition, :language,
:extension)
def multipart?
return false
end
def media_subtype
$stderr.printf("warning: media_subtype is obsolete.\n")
$stderr.printf(" use subtype instead.\n")
return subtype
end
end
class BodyTypeMultipart < Struct.new(:media_type, :subtype,
:parts,
:param, :disposition, :language,
:extension)
def multipart?
return true
end
def media_subtype
$stderr.printf("warning: media_subtype is obsolete.\n")
$stderr.printf(" use subtype instead.\n")
return subtype
end
end
class ResponseParser def parse(str)
@str = str
@pos = 0
@lex_state = EXPR_BEG
@token = nil
return response
end
private
EXPR_BEG = :EXPR_BEG
EXPR_DATA = :EXPR_DATA
EXPR_TEXT = :EXPR_TEXT
EXPR_RTEXT = :EXPR_RTEXT
EXPR_CTEXT = :EXPR_CTEXT
T_SPACE = :SPACE
T_NIL = :NIL
T_NUMBER = :NUMBER
T_ATOM = :ATOM
T_QUOTED = :QUOTED
T_LPAR = :LPAR
T_RPAR = :RPAR
T_BSLASH = :BSLASH
T_STAR = :STAR
T_LBRA = :LBRA
T_RBRA = :RBRA
T_LITERAL = :LITERAL
T_PLUS = :PLUS
T_PERCENT = :PERCENT
T_CRLF = :CRLF
T_EOF = :EOF
T_TEXT = :TEXT
BEG_REGEXP = /\G(?:\
(?(?(?(?(?(?(?(?(?(?(?(?(?(?(?(?
DATA_REGEXP = /\G(?:\
(?(?(?(?(?(?(?
TEXT_REGEXP = /\G(?:\
(?
RTEXT_REGEXP = /\G(?:\
(?(?
CTEXT_REGEXP = /\G(?:\
(?
Token = Struct.new(:symbol, :value)
def response
token = lookahead
case token.symbol
when T_PLUS
result = continue_req
when T_STAR
result = response_untagged
else
result = response_tagged
end
match(T_CRLF)
match(T_EOF)
return result
end
def continue_req
match(T_PLUS)
match(T_SPACE)
return ContinuationRequest.new(resp_text, @str)
end
def response_untagged
match(T_STAR)
match(T_SPACE)
token = lookahead
if token.symbol == T_NUMBER
return numeric_response
elsif token.symbol == T_ATOM
case token.value
when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
return response_cond
when /\A(?:FLAGS)\z/ni
return flags_response
when /\A(?:LIST|LSUB)\z/ni
return list_response
when /\A(?:QUOTA)\z/ni
return getquota_response
when /\A(?:QUOTAROOT)\z/ni
return getquotaroot_response
when /\A(?:ACL)\z/ni
return getacl_response
when /\A(?:SEARCH|SORT)\z/ni
return search_response
when /\A(?:THREAD)\z/ni
return thread_response
when /\A(?:STATUS)\z/ni
return status_response
when /\A(?:CAPABILITY)\z/ni
return capability_response
else
return text_response
end
else
parse_error("unexpected token %s", token.symbol)
end
end
def response_tagged
tag = atom
match(T_SPACE)
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return TaggedResponse.new(tag, name, resp_text, @str)
end
def response_cond
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return UntaggedResponse.new(name, resp_text, @str)
end
def numeric_response
n = number
match(T_SPACE)
token = match(T_ATOM)
name = token.value.upcase
case name
when "EXISTS", "RECENT", "EXPUNGE"
return UntaggedResponse.new(name, n, @str)
when "FETCH"
shift_token
match(T_SPACE)
data = FetchData.new(n, msg_att)
return UntaggedResponse.new(name, data, @str)
end
end
def msg_att
match(T_LPAR)
attr = {}
while true
token = lookahead
case token.symbol
when T_RPAR
shift_token
break
when T_SPACE
shift_token
token = lookahead
end
case token.value
when /\A(?:ENVELOPE)\z/ni
name, val = envelope_data
when /\A(?:FLAGS)\z/ni
name, val = flags_data
when /\A(?:INTERNALDATE)\z/ni
name, val = internaldate_data
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
name, val = rfc822_text
when /\A(?:RFC822\.SIZE)\z/ni
name, val = rfc822_size
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
name, val = body_data
when /\A(?:UID)\z/ni
name, val = uid_data
else
parse_error("unknown attribute `%s'", token.value)
end
attr[name] = val
end
return attr
end
def envelope_data
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return name, envelope
end
def envelope
@lex_state = EXPR_DATA
token = lookahead
if token.symbol == T_NIL
shift_token
result = nil
else
match(T_LPAR)
date = nstring
match(T_SPACE)
subject = nstring
match(T_SPACE)
from = address_list
match(T_SPACE)
sender = address_list
match(T_SPACE)
reply_to = address_list
match(T_SPACE)
to = address_list
match(T_SPACE)
cc = address_list
match(T_SPACE)
bcc = address_list
match(T_SPACE)
in_reply_to = nstring
match(T_SPACE)
message_id = nstring
match(T_RPAR)
result = Envelope.new(date, subject, from, sender, reply_to,
to, cc, bcc, in_reply_to, message_id)
end
@lex_state = EXPR_BEG
return result
end
def flags_data
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return name, flag_list
end
def internaldate_data
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
token = match(T_QUOTED)
return name, token.value
end
def rfc822_text
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return name, nstring
end
def rfc822_size
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return name, number
end
def body_data
token = match(T_ATOM)
name = token.value.upcase
token = lookahead
if token.symbol == T_SPACE
shift_token
return name, body
end
name.concat(section)
token = lookahead
if token.symbol == T_ATOM
name.concat(token.value)
shift_token
end
match(T_SPACE)
data = nstring
return name, data
end
def body
@lex_state = EXPR_DATA
token = lookahead
if token.symbol == T_NIL
shift_token
result = nil
else
match(T_LPAR)
token = lookahead
if token.symbol == T_LPAR
result = body_type_mpart
else
result = body_type_1part
end
match(T_RPAR)
end
@lex_state = EXPR_BEG
return result
end
def body_type_1part
token = lookahead
case token.value
when /\A(?:TEXT)\z/ni
return body_type_text
when /\A(?:MESSAGE)\z/ni
return body_type_msg
else
return body_type_basic
end
end
def body_type_basic
mtype, msubtype = media_type
token = lookahead
if token.symbol == T_RPAR
return BodyTypeBasic.new(mtype, msubtype)
end
match(T_SPACE)
param, content_id, desc, enc, size = body_fields
md5, disposition, language, extension = body_ext_1part
return BodyTypeBasic.new(mtype, msubtype,
param, content_id,
desc, enc, size,
md5, disposition, language, extension)
end
def body_type_text
mtype, msubtype = media_type
match(T_SPACE)
param, content_id, desc, enc, size = body_fields
match(T_SPACE)
lines = number
md5, disposition, language, extension = body_ext_1part
return BodyTypeText.new(mtype, msubtype,
param, content_id,
desc, enc, size,
lines,
md5, disposition, language, extension)
end
def body_type_msg
mtype, msubtype = media_type
match(T_SPACE)
param, content_id, desc, enc, size = body_fields
match(T_SPACE)
env = envelope
match(T_SPACE)
b = body
match(T_SPACE)
lines = number
md5, disposition, language, extension = body_ext_1part
return BodyTypeMessage.new(mtype, msubtype,
param, content_id,
desc, enc, size,
env, b, lines,
md5, disposition, language, extension)
end
def body_type_mpart
parts = []
while true
token = lookahead
if token.symbol == T_SPACE
shift_token
break
end
parts.push(body)
end
mtype = "MULTIPART"
msubtype = case_insensitive_string
param, disposition, language, extension = body_ext_mpart
return BodyTypeMultipart.new(mtype, msubtype, parts,
param, disposition, language,
extension)
end
def media_type
mtype = case_insensitive_string
match(T_SPACE)
msubtype = case_insensitive_string
return mtype, msubtype
end
def body_fields
param = body_fld_param
match(T_SPACE)
content_id = nstring
match(T_SPACE)
desc = nstring
match(T_SPACE)
enc = case_insensitive_string
match(T_SPACE)
size = number
return param, content_id, desc, enc, size
end
def body_fld_param
token = lookahead
if token.symbol == T_NIL
shift_token
return nil
end
match(T_LPAR)
param = {}
while true
token = lookahead
case token.symbol
when T_RPAR
shift_token
break
when T_SPACE
shift_token
end
name = case_insensitive_string
match(T_SPACE)
val = string
param[name] = val
end
return param
end
def body_ext_1part
token = lookahead
if token.symbol == T_SPACE
shift_token
else
return nil
end
md5 = nstring
token = lookahead
if token.symbol == T_SPACE
shift_token
else
return md5
end
disposition = body_fld_dsp
token = lookahead
if token.symbol == T_SPACE
shift_token
else
return md5, disposition
end
language = body_fld_lang
token = lookahead
if token.symbol == T_SPACE
shift_token
else
return md5, disposition, language
end
extension = body_extensions
return md5, disposition, language, extension
end
def body_ext_mpart
token = lookahead
if token.symbol == T_SPACE
shift_token
else
return nil
end
param = body_fld_param
token = lookahead
if token.symbol == T_SPACE
shift_token
else
return param
end
disposition = body_fld_dsp
match(T_SPACE)
language = body_fld_lang
token = lookahead
if token.symbol == T_SPACE
shift_token
else
return param, disposition, language
end
extension = body_extensions
return param, disposition, language, extension
end
def body_fld_dsp
token = lookahead
if token.symbol == T_NIL
shift_token
return nil
end
match(T_LPAR)
dsp_type = case_insensitive_string
match(T_SPACE)
param = body_fld_param
match(T_RPAR)
return ContentDisposition.new(dsp_type, param)
end
def body_fld_lang
token = lookahead
if token.symbol == T_LPAR
shift_token
result = []
while true
token = lookahead
case token.symbol
when T_RPAR
shift_token
return result
when T_SPACE
shift_token
end
result.push(case_insensitive_string)
end
else
lang = nstring
if lang
return lang.upcase
else
return lang
end
end
end
def body_extensions
result = []
while true
token = lookahead
case token.symbol
when T_RPAR
return result
when T_SPACE
shift_token
end
result.push(body_extension)
end
end
def body_extension
token = lookahead
case token.symbol
when T_LPAR
shift_token
result = body_extensions
match(T_RPAR)
return result
when T_NUMBER
return number
else
return nstring
end
end
def section
str = ""
token = match(T_LBRA)
str.concat(token.value)
token = match(T_ATOM, T_NUMBER, T_RBRA)
if token.symbol == T_RBRA
str.concat(token.value)
return str
end
str.concat(token.value)
token = lookahead
if token.symbol == T_SPACE
shift_token
str.concat(token.value)
token = match(T_LPAR)
str.concat(token.value)
while true
token = lookahead
case token.symbol
when T_RPAR
str.concat(token.value)
shift_token
break
when T_SPACE
shift_token
str.concat(token.value)
end
str.concat(format_string(astring))
end
end
token = match(T_RBRA)
str.concat(token.value)
return str
end
def format_string(str)
case str
when ""
return '""'
when /[\x80-\xff\r\n]/n
return "{" + str.length.to_s + "}" + CRLF + str
when /[(){ \x00-\x1f\x7f%*"\\]/n
# quoted string
return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
else
# atom
return str
end
end
def uid_data
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return name, number
end
def text_response
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
@lex_state = EXPR_TEXT
token = match(T_TEXT)
@lex_state = EXPR_BEG
return UntaggedResponse.new(name, token.value)
end
def flags_response
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return UntaggedResponse.new(name, flag_list, @str)
end
def list_response
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return UntaggedResponse.new(name, mailbox_list, @str)
end
def mailbox_list
attr = flag_list
match(T_SPACE)
token = match(T_QUOTED, T_NIL)
if token.symbol == T_NIL
delim = nil
else
delim = token.value
end
match(T_SPACE)
name = astring
return MailboxList.new(attr, delim, name)
end
def getquota_response
# If quota never established, get back
# `NO Quota root does not exist'.
# If quota removed, get `()' after the
# folder spec with no mention of `STORAGE'.
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
mailbox = astring
match(T_SPACE)
match(T_LPAR)
token = lookahead
case token.symbol
when T_RPAR
shift_token
data = MailboxQuota.new(mailbox, nil, nil)
return UntaggedResponse.new(name, data, @str)
when T_ATOM
shift_token
match(T_SPACE)
token = match(T_NUMBER)
usage = token.value
match(T_SPACE)
token = match(T_NUMBER)
quota = token.value
match(T_RPAR)
data = MailboxQuota.new(mailbox, usage, quota)
return UntaggedResponse.new(name, data, @str)
else
parse_error("unexpected token %s", token.symbol)
end
end
def getquotaroot_response
# Similar to getquota, but only admin can use getquota.
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
mailbox = astring
quotaroots = []
while true
token = lookahead
break unless token.symbol == T_SPACE
shift_token
quotaroots.push(astring)
end
data = MailboxQuotaRoot.new(mailbox, quotaroots)
return UntaggedResponse.new(name, data, @str)
end
def getacl_response
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
mailbox = astring
data = []
token = lookahead
if token.symbol == T_SPACE
shift_token
while true
token = lookahead
case token.symbol
when T_CRLF
break
when T_SPACE
shift_token
end
user = astring
match(T_SPACE)
rights = astring
##XXX data.push([user, rights])
data.push(MailboxACLItem.new(user, rights))
end
end
return UntaggedResponse.new(name, data, @str)
end
def search_response
token = match(T_ATOM)
name = token.value.upcase
token = lookahead
if token.symbol == T_SPACE
shift_token
data = []
while true
token = lookahead
case token.symbol
when T_CRLF
break
when T_SPACE
shift_token
end
data.push(number)
end
else
data = []
end
return UntaggedResponse.new(name, data, @str)
end
def thread_response
token = match(T_ATOM)
name = token.value.upcase
token = lookahead
if token.symbol == T_SPACE
threads = []
while true
shift_token
token = lookahead
case token.symbol
when T_LPAR
threads << thread_branch(token)
when T_CRLF
break
end
end
else
# no member
threads = []
end
return UntaggedResponse.new(name, threads, @str)
end
def thread_branch(token)
rootmember = nil
lastmember = nil
while true
shift_token # ignore first T_LPAR
token = lookahead
case token.symbol
when T_NUMBER
# new member
newmember = ThreadMember.new(number, [])
if rootmember.nil?
rootmember = newmember
else
lastmember.children << newmember
end
lastmember = newmember
when T_SPACE
# do nothing
when T_LPAR
if rootmember.nil?
# dummy member
lastmember = rootmember = ThreadMember.new(nil, [])
end
lastmember.children << thread_branch(token)
when T_RPAR
break
end
end
return rootmember
end
def status_response
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
mailbox = astring
match(T_SPACE)
match(T_LPAR)
attr = {}
while true
token = lookahead
case token.symbol
when T_RPAR
shift_token
break
when T_SPACE
shift_token
end
token = match(T_ATOM)
key = token.value.upcase
match(T_SPACE)
val = number
attr[key] = val
end
data = StatusData.new(mailbox, attr)
return UntaggedResponse.new(name, data, @str)
end
def capability_response
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
data = []
while true
token = lookahead
case token.symbol
when T_CRLF
break
when T_SPACE
shift_token
end
data.push(atom.upcase)
end
return UntaggedResponse.new(name, data, @str)
end
def resp_text
@lex_state = EXPR_RTEXT
token = lookahead
if token.symbol == T_LBRA
code = resp_text_code
else
code = nil
end
token = match(T_TEXT)
@lex_state = EXPR_BEG
return ResponseText.new(code, token.value)
end
def resp_text_code
@lex_state = EXPR_BEG
match(T_LBRA)
token = match(T_ATOM)
name = token.value.upcase
case name
when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
result = ResponseCode.new(name, nil)
when /\A(?:PERMANENTFLAGS)\z/n
match(T_SPACE)
result = ResponseCode.new(name, flag_list)
when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
match(T_SPACE)
result = ResponseCode.new(name, number)
else
token = lookahead
if token.symbol == T_SPACE
shift_token
@lex_state = EXPR_CTEXT
token = match(T_TEXT)
@lex_state = EXPR_BEG
result = ResponseCode.new(name, token.value)
else
result = ResponseCode.new(name, nil)
end
end
match(T_RBRA)
@lex_state = EXPR_RTEXT
return result
end
def address_list
token = lookahead
if token.symbol == T_NIL
shift_token
return nil
else
result = []
match(T_LPAR)
while true
token = lookahead
case token.symbol
when T_RPAR
shift_token
break
when T_SPACE
shift_token
end
result.push(address)
end
return result
end
end
ADDRESS_REGEXP = /\G\
(?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
(?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
(?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
(?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
\)/ni
def address
match(T_LPAR)
if @str.index(ADDRESS_REGEXP, @pos)
# address does not include literal.
@pos = $~.end(0)
name = $1
route = $2
mailbox = $3
host = $4
for s in [name, route, mailbox, host]
if s
s.gsub!(/\\(["\\])/n, "\\1")
end
end
else
name = nstring
match(T_SPACE)
route = nstring
match(T_SPACE)
mailbox = nstring
match(T_SPACE)
host = nstring
match(T_RPAR)
end
return Address.new(name, route, mailbox, host)
end
FLAG_REGEXP = /\
(?(?
def flag_list
if @str.index(/\(([^)]*)\)/ni, @pos)
@pos = $~.end(0)
return $1.scan(FLAG_REGEXP).collect { |flag, atom|
atom || flag.capitalize.intern
}
else
parse_error("invalid flag list")
end
end
def nstring
token = lookahead
if token.symbol == T_NIL
shift_token
return nil
else
return string
end
end
def astring
token = lookahead
if string_token?(token)
return string
else
return atom
end
end
def string
token = lookahead
if token.symbol == T_NIL
shift_token
return nil
end
token = match(T_QUOTED, T_LITERAL)
return token.value
end
STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
def string_token?(token)
return STRING_TOKENS.include?(token.symbol)
end
def case_insensitive_string
token = lookahead
if token.symbol == T_NIL
shift_token
return nil
end
token = match(T_QUOTED, T_LITERAL)
return token.value.upcase
end
def atom
result = ""
while true
token = lookahead
if atom_token?(token)
result.concat(token.value)
shift_token
else
if result.empty?
parse_error("unexpected token %s", token.symbol)
else
return result
end
end
end
end
ATOM_TOKENS = [
T_ATOM,
T_NUMBER,
T_NIL,
T_LBRA,
T_RBRA,
T_PLUS
]
def atom_token?(token)
return ATOM_TOKENS.include?(token.symbol)
end
def number
token = lookahead
if token.symbol == T_NIL
shift_token
return nil
end
token = match(T_NUMBER)
return token.value.to_i
end
def nil_atom
match(T_NIL)
return nil
end
def match(*args)
token = lookahead
unless args.include?(token.symbol)
parse_error('unexpected token %s (expected %s)',
token.symbol.id2name,
args.collect {|i| i.id2name}.join(" or "))
end
shift_token
return token
end
def lookahead
unless @token
@token = next_token
end
return @token
end
def shift_token
@token = nil
end
def next_token
case @lex_state
when EXPR_BEG
if @str.index(BEG_REGEXP, @pos)
@pos = $~.end(0)
if $1
return Token.new(T_SPACE, $+)
elsif $2
return Token.new(T_NIL, $+)
elsif $3
return Token.new(T_NUMBER, $+)
elsif $4
return Token.new(T_ATOM, $+)
elsif $5
return Token.new(T_QUOTED,
$+.gsub(/\\(["\\])/n, "\\1"))
elsif $6
return Token.new(T_LPAR, $+)
elsif $7
return Token.new(T_RPAR, $+)
elsif $8
return Token.new(T_BSLASH, $+)
elsif $9
return Token.new(T_STAR, $+)
elsif $10
return Token.new(T_LBRA, $+)
elsif $11
return Token.new(T_RBRA, $+)
elsif $12
len = $+.to_i
val = @str[@pos, len]
@pos += len
return Token.new(T_LITERAL, val)
elsif $13
return Token.new(T_PLUS, $+)
elsif $14
return Token.new(T_PERCENT, $+)
elsif $15
return Token.new(T_CRLF, $+)
elsif $16
return Token.new(T_EOF, $+)
else
parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
end
else
@str.index(/\S*/n, @pos)
parse_error("unknown token - %s", $&.dump)
end
when EXPR_DATA
if @str.index(DATA_REGEXP, @pos)
@pos = $~.end(0)
if $1
return Token.new(T_SPACE, $+)
elsif $2
return Token.new(T_NIL, $+)
elsif $3
return Token.new(T_NUMBER, $+)
elsif $4
return Token.new(T_QUOTED,
$+.gsub(/\\(["\\])/n, "\\1"))
elsif $5
len = $+.to_i
val = @str[@pos, len]
@pos += len
return Token.new(T_LITERAL, val)
elsif $6
return Token.new(T_LPAR, $+)
elsif $7
return Token.new(T_RPAR, $+)
else
parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
end
else
@str.index(/\S*/n, @pos)
parse_error("unknown token - %s", $&.dump)
end
when EXPR_TEXT
if @str.index(TEXT_REGEXP, @pos)
@pos = $~.end(0)
if $1
return Token.new(T_TEXT, $+)
else
parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
end
else
@str.index(/\S*/n, @pos)
parse_error("unknown token - %s", $&.dump)
end
when EXPR_RTEXT
if @str.index(RTEXT_REGEXP, @pos)
@pos = $~.end(0)
if $1
return Token.new(T_LBRA, $+)
elsif $2
return Token.new(T_TEXT, $+)
else
parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
end
else
@str.index(/\S*/n, @pos)
parse_error("unknown token - %s", $&.dump)
end
when EXPR_CTEXT
if @str.index(CTEXT_REGEXP, @pos)
@pos = $~.end(0)
if $1
return Token.new(T_TEXT, $+)
else
parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
end
else
@str.index(/\S*/n, @pos) parse_error("unknown token - %s", $&.dump)
end
else
parse_error("illegal @lex_state - %s", @lex_state.inspect)
end
end
def parse_error(fmt, *args)
if IMAP.debug
$stderr.printf("@str: %s\n", @str.dump)
$stderr.printf("@pos: %d\n", @pos)
$stderr.printf("@lex_state: %s\n", @lex_state)
if @token
$stderr.printf("@token.symbol: %s\n", @token.symbol)
$stderr.printf("@token.value: %s\n", @token.value.inspect)
end
end
raise ResponseParseError, format(fmt, *args)
end
end
class LoginAuthenticator
def process(data)
case @state
when STATE_USER
@state = STATE_PASSWORD
return @user
when STATE_PASSWORD
return @password
end
end
private
STATE_USER = :USER
STATE_PASSWORD = :PASSWORD
def initialize(user, password)
@user = user
@password = password
@state = STATE_USER
end
end
add_authenticator "LOGIN", LoginAuthenticator
class CramMD5Authenticator
def process(challenge)
digest = hmac_md5(challenge, @password)
return @user + " " + digest
end
private
def initialize(user, password)
@user = user
@password = password
end
def hmac_md5(text, key)
if key.length > 64
key = Digest::MD5.digest(key)
end
k_ipad = key + "\0" * (64 - key.length)
k_opad = key + "\0" * (64 - key.length)
for i in 0..63
k_ipad[i] ^= 0x36
k_opad[i] ^= 0x5c
end
digest = Digest::MD5.digest(k_ipad + text)
return Digest::MD5.hexdigest(k_opad + digest)
end
end
add_authenticator "CRAM-MD5", CramMD5Authenticator
class Error < StandardError
end
class DataFormatError < Error
end
class ResponseParseError < Error
end
class ResponseError < Error
end
class NoResponseError < ResponseError
end
class BadResponseError < ResponseError
end
class ByeResponseError < ResponseError
end
end
end
if __FILE__ == $0
require "getoptlong"
$stdout.sync = true
$port = nil
$user = ENV["USER"] || ENV["LOGNAME"]
$auth = "login"
$ssl = false
def usage
$stderr.print <<EOF
usage: #{$0} [options] <host>
--help print this message
--port=PORT specifies port
--user=USER specifies user
--auth=AUTH specifies auth type
--ssl use ssl
EOF
end
def get_password
print "password: "
system("stty", "-echo")
begin
return gets.chop
ensure
system("stty", "echo")
print "\n"
end
end
def get_command
printf("%s@%s> ", $user, $host)
if line = gets
return line.strip.split(/\s+/)
else
return nil
end
end
parser = GetoptLong.new
parser.set_options(['--debug', GetoptLong::NO_ARGUMENT],
['--help', GetoptLong::NO_ARGUMENT],
['--port', GetoptLong::REQUIRED_ARGUMENT],
['--user', GetoptLong::REQUIRED_ARGUMENT],
['--auth', GetoptLong::REQUIRED_ARGUMENT],
['--ssl', GetoptLong::NO_ARGUMENT])
begin
parser.each_option do |name, arg|
case name
when "--port"
$port = arg
when "--user"
$user = arg
when "--auth"
$auth = arg
when "--ssl"
$ssl = true
when "--debug"
Net::IMAP.debug = true
when "--help"
usage
exit(1)
end
end
rescue
usage
exit(1)
end
$host = ARGV.shift
unless $host
usage
exit(1)
end
$port ||= $ssl ? 993 : 143
imap = Net::IMAP.new($host, $port, $ssl)
begin
password = get_password
imap.authenticate($auth, $user, password)
while true
cmd, *args = get_command
break unless cmd
begin
case cmd
when "list"
for mbox in imap.list("", args[0] || "*")
if mbox.attr.include?(Net::IMAP::NOSELECT)
prefix = "!"
elsif mbox.attr.include?(Net::IMAP::MARKED)
prefix = "*"
else
prefix = " "
end
print prefix, mbox.name, "\n"
end
when "select"
imap.select(args[0] || "inbox")
print "ok\n"
when "close"
imap.close
print "ok\n"
when "summary"
unless messages = imap.responses["EXISTS"][-1]
puts "not selected"
next
end
if messages > 0
for data in imap.fetch(1..-1, ["ENVELOPE"])
print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
end
else
puts "no message"
end
when "fetch"
if args[0]
data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
puts data.attr["RFC822.HEADER"]
puts data.attr["RFC822.TEXT"]
else
puts "missing argument"
end
when "logout", "exit", "quit"
break
when "help", "?"
print <<EOF
list [pattern] list mailboxes
select [mailbox] select mailbox
close close mailbox
summary display summary
fetch [msgno] display message
logout logout
help, ? display help message
EOF
else
print "unknown command: ", cmd, "\n"
end
rescue Net::IMAP::Error
puts $!
end
end
ensure
imap.logout
imap.disconnect
end
end