Codebase list feed2imap / ffc7f46
Merge tag 'upstream/1.1' Upstream version 1.1 # gpg: Signature made Dom 25 Ago 2013 19:36:57 BRT using DSA key ID 0F9CB28F # gpg: Good signature from "Antonio Terceiro <terceiro@softwarelivre.org>" # gpg: aka "Antonio Terceiro (DCC/UFBA) <terceiro@dcc.ufba.br>" # gpg: aka "Antonio Terceiro (Colivre) <terceiro@colivre.coop.br>" # gpg: aka "Antonio Terceiro <terceiro@debian.org>" # gpg: aka "Antonio Terceiro <antonio.terceiro@linaro.org>" Antonio Terceiro 10 years ago
18 changed file(s) with 324 addition(s) and 3689 deletion(s). Raw diff Collapse all Expand all
00 require 'rake/testtask'
1 require 'rake/rdoctask'
1 require 'rdoc/task'
22 require 'rake/packagetask'
33 require 'rake'
44 require 'find'
55
6 task :default => [:package]
6 task :default => [:test]
77
88 PKG_NAME = 'feed2imap'
9 PKG_VERSION = '1.0'
9 PKG_VERSION = '1.1'
1010 PKG_FILES = [ 'ChangeLog', 'README', 'COPYING', 'setup.rb', 'Rakefile']
1111 Find.find('bin/', 'lib/', 'test/', 'data/') do |f|
1212 if FileTest.directory?(f) and f =~ /\.svn/
1818 Rake::TestTask.new do |t|
1919 t.libs << "libs/feed2imap"
2020 t.libs << "test"
21 t.test_files = FileList['test/tc_*.rb']
21 t.test_files = FileList['test/tc_*.rb'] - ['test/tc_httpfetcher.rb']
2222 end
2323
24 Rake::RDocTask.new do |rd|
24 RDoc::Task.new do |rd|
2525 rd.main = 'README'
2626 rd.rdoc_files.include('lib/*.rb', 'lib/feed2imap/*.rb')
2727 rd.options << '--all'
4040
4141 # "Gem" part of the Rakefile
4242 begin
43 require 'rake/gempackagetask'
43 require 'rubygems/package_task'
4444
4545 spec = Gem::Specification.new do |s|
4646 s.platform = Gem::Platform::RUBY
4949 s.version = PKG_VERSION
5050 s.requirements << 'feedparser'
5151 s.require_path = 'lib'
52 s.executables = PKG_FILES.grep(%r{\Abin\/.}).map { |bin|
53 bin.gsub(%r{\Abin/}, '')
54 }
5255 s.files = PKG_FILES
5356 s.description = "RSS/Atom feed aggregator"
57 s.authors = ['Lucas Nussbaum']
5458 end
5559
56 Rake::GemPackageTask.new(spec) do |pkg|
60 Gem::PackageTask.new(spec) do |pkg|
5761 pkg.need_zip = true
5862 pkg.need_tar = true
5963 end
66 # debug-updated: (for debugging purposes) if true, display a lot of information
77 # about the "updated-items" algorithm.
88 # include-images: download images and include them in the mail? (true/false)
9 # reupload-if-updated: when an item is updated, and was previously deleted,
10 # reupload it? (true/false, default true)
911 # default-email: default email address in the format foo@example.com
1012 # disable-ssl-verification: disable SSL certification when connecting
1113 # to IMAPS accounts (true/false)
14 # timeout: time before getting timeout when fetching feeds (default 30) in seconds
1215 #
1316 # Per-feed options:
1417 # name: name of the feed (must be unique)
1922 # feed will be fetched
2023 # disable: if set to something, the feed will be ignored
2124 # include-images: download images and include them in the mail? (true/false)
25 # reupload-if-updated: when an item is updated, and was previously deleted,
26 # reupload it? (true/false, default true)
2227 # always-new: feed2imap tries to use a clever algorithm to determine whether
2328 # an item is new or has been updated. It doesn't work well with some web apps
2429 # like mediawiki. When this flag is enabled, all items which don't match
6267 # - name: test2
6368 # target: [ *target, 'test2' ]
6469 # ...
70
71 # vim: ft=yaml:sts=2:expandtab
192192 @itemstemp.unshift(j)
193193 break
194194 end
195 end
196 next if found
197 if not always_new
198 # Try to find an updated item
199 @items.each do |j|
200 # Do we need a better heuristic ?
201 if j.is_ancestor_of(i)
202 i.cacheditem.index = j.index
203 i.cacheditem.updated = true
204 updateditems.push(i)
205 found = true
206 # let's put j in front of itemstemp
207 @itemstemp.delete(j)
208 @itemstemp.unshift(i.cacheditem)
209 break
210 end
195 # If we didn't find exact match, try to check if we have an update
196 if j.is_ancestor_of(i)
197 i.cacheditem.index = j.index
198 i.cacheditem.updated = true
199 updateditems.push(i)
200 found = true
201 # let's put j in front of itemstemp
202 @itemstemp.delete(j)
203 @itemstemp.unshift(i.cacheditem)
204 break
211205 end
212206 end
213207 next if found
2222 require 'feed2imap/maildir'
2323 require 'etc'
2424 require 'socket'
25 require 'set'
2526
2627 # Default cache file
2728 DEFCACHE = ENV['HOME'] + '/.feed2imap.cache'
3233
3334 # Feed2imap configuration
3435 class F2IConfig
35 attr_reader :imap_accounts, :cache, :feeds, :dumpdir, :updateddebug, :max_failures, :include_images, :default_email, :hostname
36 attr_reader :imap_accounts, :cache, :feeds, :dumpdir, :updateddebug, :max_failures, :include_images, :default_email, :hostname, :reupload_if_updated, :parts, :timeout
3637
3738 # Load the configuration from the IO stream
3839 # TODO should do some sanity check on the data read.
4344 @conf['feeds'] ||= []
4445 @feeds = []
4546 @max_failures = (@conf['max-failures'] || 10).to_i
46 @updateddebug = (@conf['debug-updated'] and @conf['debug-updated'] != 'false')
47 @include_images = (@conf['include-images'] and @conf['include-images'] != 'false')
47
48 @updateddebug = false
49 @updateddebug = @conf['debug-updated'] if @conf.has_key?('debug-updated')
50
51 @parts = %w(text html)
52 @parts = Array(@conf['parts']) if @conf.has_key?('parts') && !@conf['parts'].empty?
53 @parts = Set.new(@parts)
54
55 @include_images = true
56 @include_images = @conf['include-images'] if @conf.has_key?('include-images')
57 @parts << 'html' if @include_images && ! @parts.include?('html')
58
59 @reupload_if_updated = true
60 @reupload_if_updated = @conf['reupload-if-updated'] if @conf.has_key?('reupload-if-updated')
61
62 @timeout = if @conf['timeout'] == nil then 30 else @conf['timeout'].to_i end
63
4864 @default_email = (@conf['default-email'] || "#{LOGNAME}@#{HOSTNAME}")
49 ImapAccount.no_ssl_verify = (@conf['disable-ssl-verification'] and @conf['disable-ssl-verification'] != 'false')
65 ImapAccount.no_ssl_verify = (@conf.has_key?('disable-ssl-verification') and @conf['disable-ssl-verification'] == true)
5066 @hostname = HOSTNAME # FIXME: should this be configurable as well?
5167 @imap_accounts = ImapAccounts::new
5268 maildir_account = MaildirAccount::new
5470 if f['disable'].nil?
5571 uri = URI::parse(f['target'].to_s)
5672 path = URI::unescape(uri.path)
57 path = path[1..-1] if path[0,1] == '/'
5873 if uri.scheme == 'maildir'
5974 @feeds.push(ConfigFeed::new(f, maildir_account, path, self))
6075 else
76 # remove leading slash from IMAP mailbox names
77 path = path[1..-1] if path[0,1] == '/'
6178 @feeds.push(ConfigFeed::new(f, @imap_accounts.add_account(uri), path, self))
6279 end
6380 end
93110
94111 # A configured feed. simple data container.
95112 class ConfigFeed
96 attr_reader :name, :url, :imapaccount, :folder, :always_new, :execurl, :filter, :ignore_hash, :dumpdir, :wrapto, :include_images
113 attr_reader :name, :url, :imapaccount, :folder, :always_new, :execurl, :filter, :ignore_hash, :dumpdir, :wrapto, :include_images, :reupload_if_updated
97114 attr_accessor :body
98115
99116 def initialize(f, imapaccount, folder, f2iconfig)
100117 @name = f['name']
101118 @url = f['url']
102119 @url.sub!(/^feed:/, '') if @url =~ /^feed:/
103 @imapaccount, @folder = imapaccount, folder
120 @imapaccount = imapaccount
121 @folder = encode_utf7 folder
104122 @freq = f['min-frequency']
105 @always_new = (f['always-new'] and f['always-new'] != 'false')
123
124 @always_new = false
125 @always_new = f['always-new'] if f.has_key?('always-new')
126
106127 @execurl = f['execurl']
107128 @filter = f['filter']
108 @ignore_hash = f['ignore-hash'] || false
129
130 @ignore_hash = false
131 @ignore_hash = f['ignore-hash'] if f.has_key?('ignore-hash')
132
109133 @freq = @freq.to_i if @freq
110134 @dumpdir = f['dumpdir'] || nil
111135 @wrapto = if f['wrapto'] == nil then 72 else f['wrapto'].to_i end
136
112137 @include_images = f2iconfig.include_images
113 if f['include-images']
114 @include_images = (f['include-images'] != 'false')
115 end
138 @include_images = f['include-images'] if f.has_key?('include-images')
139
140 @reupload_if_updated = f2iconfig.reupload_if_updated
141 @reupload_if_updated = f['reupload-if-updated'] if f.has_key?('reupload-if-updated')
142
116143 end
117144
118145 def needfetch(lastcheck)
119146 return true if @freq.nil?
120147 return (lastcheck + @freq * 3600) < Time::now
121148 end
149
150 def encode_utf7(s)
151 if "foo".respond_to?(:force_encoding)
152 return Net::IMAP::encode_utf7 s
153 else
154 # this is a copy of the Net::IMAP::encode_utf7 w/o the force_encoding
155 return s.gsub(/(&)|([^\x20-\x7e]+)/u) {
156 if $1
157 "&-"
158 else
159 base64 = [$&.unpack("U*").pack("n*")].pack("m")
160 "&" + base64.delete("=\n").tr("/", ",") + "-"
161 end }
162 end
163 end
122164 end
120120 end
121121 fetch_start = Time::now
122122 if feed.url
123 s = HTTPFetcher::fetch(feed.url, @cache.get_last_check(feed.name))
123 fetcher = HTTPFetcher::new
124 fetcher::timeout = @config.timeout
125 s = fetcher::fetch(feed.url, @cache.get_last_check(feed.name))
124126 elsif feed.execurl
125127 # avoid running more than one command at the same time.
126128 # We need it because the called command might not be
220222 next
221223 end
222224 begin
223 feed = FeedParser::Feed::new(f.body)
225 feed = FeedParser::Feed::new(f.body.force_encoding('UTF-8'))
224226 rescue Exception
225227 n = @cache.parse_failed(f.name)
226228 m = "Error while parsing #{f.name}: #{$!} (failed #{n} times)"
246248 id = "<#{fn}-#{i.cacheditem.index}@#{@config.hostname}>"
247249 email = item_to_mail(@config, i, id, true, f.name, f.include_images, f.wrapto)
248250 f.imapaccount.updatemail(f.folder, email,
249 id, i.date || Time::new)
251 id, i.date || Time::new, f.reupload_if_updated)
250252 end
251253 # reverse is needed to upload older items first (fixes gna#8986)
252254 newitems.reverse.each do |i|
1616 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1717 =end
1818
19 require 'zlib'
1920 require 'net/http'
2021 # get openssl if available
2122 begin
3233
3334 # Class used to retrieve the feed over HTTP
3435 class HTTPFetcher
35 def HTTPFetcher::fetcher(baseuri, uri, lastcheck, recursion)
36
37 @timeout = 30 # should be enough for everybody...
38
39 def timeout=(value)
40 @timeout = value
41 end
42
43 def fetcher(baseuri, uri, lastcheck, recursion)
3644 proxy_host = nil
3745 proxy_port = nil
3846 proxy_user = nil
4856 proxy_port,
4957 proxy_user,
5058 proxy_pass ).new(uri.host, uri.port)
51 http.read_timeout = 30 # should be enough for everybody...
52 http.open_timeout = 30
59 http.read_timeout = @timeout
60 http.open_timeout = @timeout
5361 if uri.scheme == 'https'
5462 http.use_ssl = true
5563 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
6068 useragent = 'Feed2Imap http://home.gna.org/feed2imap/'
6169 end
6270
63 if lastcheck == Time::at(0)
64 req = Net::HTTP::Get::new(uri.request_uri, {'User-Agent' => useragent })
65 else
66 req = Net::HTTP::Get::new(uri.request_uri, {'User-Agent' => useragent, 'If-Modified-Since' => lastcheck.httpdate})
71 headers = {
72 'User-Agent' => useragent,
73 'Accept-Encoding' => 'gzip',
74 }
75 if lastcheck != Time::at(0)
76 headers.merge!('If-Modified-Since' => lastcheck.httpdate)
6777 end
78 req = Net::HTTP::Get::new(uri.request_uri, headers)
6879 if uri.userinfo
6980 login, pw = uri.userinfo.split(':')
7081 req.basic_auth(login, pw)
8091 end
8192 case response
8293 when Net::HTTPSuccess
83 return response.body
94 case response['Content-Encoding']
95 when 'gzip'
96 return Zlib::GzipReader.new(StringIO.new(response.body)).read
97 else
98 return response.body
99 end
84100 when Net::HTTPRedirection
85101 # if not modified
86102 if Net::HTTPNotModified === response
98114 end
99115 end
100116
101 def HTTPFetcher::fetch(url, lastcheck)
117 def fetch(url, lastcheck)
102118 uri = URI::parse(url)
103 return HTTPFetcher::fetcher(uri, uri, lastcheck, MAXREDIR)
119 return fetcher(uri, uri, lastcheck, MAXREDIR)
104120 end
105121 end
1717 =end
1818
1919 # Imap connection handling
20 require 'feed2imap/rubyimap'
20 require 'net/imap'
2121 begin
2222 require 'openssl'
2323 rescue LoadError
107107 end
108108
109109 # update a mail
110 def updatemail(folder, mail, id, date = Time::now)
110 def updatemail(folder, mail, id, date = Time::now, reupload_if_updated = true)
111111 create_folder_if_not_exists(folder)
112112 @connection.select(folder)
113113 searchres = @connection.search(['HEADER', 'Message-Id', id])
118118 searchres.each { |m| @connection.store(m, "+FLAGS", [:Deleted]) }
119119 @connection.expunge
120120 flags -= [ :Recent ] # avoids errors with dovecot
121 elsif not reupload_if_updated
122 # mail not present, and we don't want to re-upload it
123 return
121124 end
122125 @connection.append(folder, mail.gsub(/\n/, "\r\n"), flags, date)
123126 end
140143 d = f[0].attr['INTERNALDATE']
141144 s = f[0].attr['ENVELOPE'].subject
142145 if s =~ /^=\?utf-8\?b\?/
143 s = Base64::decode64(s.gsub(/^=\?utf-8\?b\?(.*)\?=$/, '\1')).toISO_8859_1('utf-8')
146 s = Base64::decode64(s.gsub(/^=\?utf-8\?b\?(.*)\?=$/, '\1')).force_encoding('utf-8')
147 elsif s =~ /^=\?iso-8859-1\?b\?/
148 s = Base64::decode64(s.gsub(/^=\?iso-8859-1\?b\?(.*)\?=$/, '\1')).force_encoding('iso-8859-1').encode('utf-8')
144149 end
145150 if dryrun
146151 puts "To remove: #{s} (#{d})"
7676 message.header['Subject'] = subj
7777 end
7878 end
79 textpart = RMail::Message::new
80 textpart.header['Content-Type'] = 'text/plain; charset=utf-8; format=flowed'
81 textpart.header['Content-Transfer-Encoding'] = '8bit'
82 textpart.body = item.to_text(true, wrapto, false)
83 htmlpart = RMail::Message::new
84 htmlpart.header['Content-Type'] = 'text/html; charset=utf-8'
85 htmlpart.header['Content-Transfer-Encoding'] = '8bit'
86 htmlpart.body = item.to_html
79 textpart = htmlpart = nil
80 parts = config.parts
81 if parts.include?('text')
82 textpart = parts.size == 1 ? message : RMail::Message::new
83 textpart.header['Content-Type'] = 'text/plain; charset=utf-8; format=flowed'
84 textpart.header['Content-Transfer-Encoding'] = '8bit'
85 textpart.body = item.to_text(true, wrapto, false)
86 end
87 if parts.include?('html')
88 htmlpart = parts.size == 1 ? message : RMail::Message::new
89 htmlpart.header['Content-Type'] = 'text/html; charset=utf-8'
90 htmlpart.header['Content-Transfer-Encoding'] = '8bit'
91 htmlpart.body = item.to_html
92 end
8793
8894 # inline images as attachments
8995 imgs = []
126132 imgs.each do |i|
127133 message.add_part(i)
128134 end
129 else
135 elsif parts.size != 1
130136 message.header['Content-Type'] = 'multipart/alternative'
131137 message.add_part(textpart)
132138 message.add_part(htmlpart)
1818 require 'uri'
1919 require 'fileutils'
2020 require 'fcntl'
21 require 'rmail'
22 require 'socket'
2123
2224 class MaildirAccount
2325 MYHOSTNAME = Socket.gethostname
26
27 @@seq_num = 0
2428
2529 attr_reader :uri
2630
3034 end
3135 end
3236
33 def updatemail(folder, mail, idx, date = Time::now)
37 def updatemail(folder, mail, idx, date = Time::now, reupload_if_updated = true)
3438 dir = folder_dir(folder)
3539 guarantee_maildir(dir)
3640 mail_files = find_mails(dir, idx)
3943 # get the info from the first result and delete everything
4044 info = maildir_file_info(mail_files[0])
4145 mail_files.each { |f| File.delete(File.join(dir, f)) }
46 elsif not reupload_if_updated
47 # mail not present, and we don't want to re-upload it
48 return
4249 end
4350 store_message(dir, date, info) { |f| f.puts(mail) }
4451 end
6168 next if (not flags.index('S') or
6269 flags.index('F') or
6370 mtime > recent_time)
64 File.open(fn) do |f|
65 mail = RMail::Parser.read(f)
71 mail = File.open(fn) do |f|
72 RMail::Parser.read(f)
6673 end
74 subject = mail.header['Subject']
6775 if dryrun
6876 puts "To remove: #{subject} #{mtime}"
6977 else
8391 end
8492
8593 def store_message(dir, date, info, &block)
86 # TODO: handle `date'
8794
8895 guarantee_maildir(dir)
8996
9299 timer = 30
93100 fd = nil
94101 while timer >= 0
95 new_fn = new_maildir_basefn
102 new_fn = new_maildir_basefn(date)
96103 tmp_path = File.join(dir, 'tmp', new_fn)
97104 new_path = File.join(dir, 'new', new_fn)
98105 begin
136143 Dir[File.join(subdir, '*')].each do |fn|
137144 File.open(fn) do |f|
138145 mail = RMail::Parser.read(f)
139 cache_index = mail.header['Message-Id']
140 next if not (cache_index and cache_index == idx)
141 dir_paths.push(File.join(d, File.basename(fn)))
146 cache_index = mail.header['Message-ID']
147 if cache_index && (cache_index == idx || cache_index == "<#{idx}>")
148 dir_paths.push(File.join(d, File.basename(fn)))
149 end
142150 end
143151 end
144152 end
156164 basename = File.basename(file)
157165 colon = basename.rindex(':')
158166
159 return (colon and basename.slice(colon + 1, -1))
167 return (colon and basename[colon + 1 .. -1])
160168 end
161169
162 # Shamelessly taken from
170 # Re-written and no longer shamelessly taken from
163171 # http://gitorious.org/sup/mainline/blobs/master/lib/sup/maildir.rb
164 def new_maildir_basefn
165 Kernel::srand()
166 "#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
172 def new_maildir_basefn(date)
173 fn = "#{date.to_i.to_s}.#{@@seq_num.to_s}.#{MYHOSTNAME}"
174 @@seq_num += 1
175 fn
167176 end
177
178 def maildir_file_info_flags(fn)
179 parts = fn.split(',')
180 if parts.size == 1
181 ''
182 else
183 parts.last
184 end
185 end
186
168187 end
169188
2525 module REXML
2626 module Encoding
2727 def decode(str)
28 return str.toUTF8(@encoding)
28 return str.encode(@encoding)
2929 end
3030
3131 def encode(str)
+0
-3601
lib/feed2imap/rubyimap.rb less more
0 # File fetched from
1 # http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/lib/net/imap.rb?view=log
2 # Current rev: 27336
3 ############################################################################
4 #
5 # = net/imap.rb
6 #
7 # Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org>
8 #
9 # This library is distributed under the terms of the Ruby license.
10 # You can freely distribute/modify this library.
11 #
12 # Documentation: Shugo Maeda, with RDoc conversion and overview by William
13 # Webber.
14 #
15 # See Net::IMAP for documentation.
16 #
17
18
19 require "socket"
20 require "monitor"
21 require "digest/md5"
22 require "strscan"
23 begin
24 require "openssl"
25 rescue LoadError
26 end
27
28 module Net
29
30 #
31 # Net::IMAP implements Internet Message Access Protocol (IMAP) client
32 # functionality. The protocol is described in [IMAP].
33 #
34 # == IMAP Overview
35 #
36 # An IMAP client connects to a server, and then authenticates
37 # itself using either #authenticate() or #login(). Having
38 # authenticated itself, there is a range of commands
39 # available to it. Most work with mailboxes, which may be
40 # arranged in an hierarchical namespace, and each of which
41 # contains zero or more messages. How this is implemented on
42 # the server is implementation-dependent; on a UNIX server, it
43 # will frequently be implemented as a files in mailbox format
44 # within a hierarchy of directories.
45 #
46 # To work on the messages within a mailbox, the client must
47 # first select that mailbox, using either #select() or (for
48 # read-only access) #examine(). Once the client has successfully
49 # selected a mailbox, they enter _selected_ state, and that
50 # mailbox becomes the _current_ mailbox, on which mail-item
51 # related commands implicitly operate.
52 #
53 # Messages have two sorts of identifiers: message sequence
54 # numbers, and UIDs.
55 #
56 # Message sequence numbers number messages within a mail box
57 # from 1 up to the number of items in the mail box. If new
58 # message arrives during a session, it receives a sequence
59 # number equal to the new size of the mail box. If messages
60 # are expunged from the mailbox, remaining messages have their
61 # sequence numbers "shuffled down" to fill the gaps.
62 #
63 # UIDs, on the other hand, are permanently guaranteed not to
64 # identify another message within the same mailbox, even if
65 # the existing message is deleted. UIDs are required to
66 # be assigned in ascending (but not necessarily sequential)
67 # order within a mailbox; this means that if a non-IMAP client
68 # rearranges the order of mailitems within a mailbox, the
69 # UIDs have to be reassigned. An IMAP client cannot thus
70 # rearrange message orders.
71 #
72 # == Examples of Usage
73 #
74 # === List sender and subject of all recent messages in the default mailbox
75 #
76 # imap = Net::IMAP.new('mail.example.com')
77 # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
78 # imap.examine('INBOX')
79 # imap.search(["RECENT"]).each do |message_id|
80 # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
81 # puts "#{envelope.from[0].name}: \t#{envelope.subject}"
82 # end
83 #
84 # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03"
85 #
86 # imap = Net::IMAP.new('mail.example.com')
87 # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
88 # imap.select('Mail/sent-mail')
89 # if not imap.list('Mail/', 'sent-apr03')
90 # imap.create('Mail/sent-apr03')
91 # end
92 # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
93 # imap.copy(message_id, "Mail/sent-apr03")
94 # imap.store(message_id, "+FLAGS", [:Deleted])
95 # end
96 # imap.expunge
97 #
98 # == Thread Safety
99 #
100 # Net::IMAP supports concurrent threads. For example,
101 #
102 # imap = Net::IMAP.new("imap.foo.net", "imap2")
103 # imap.authenticate("cram-md5", "bar", "password")
104 # imap.select("inbox")
105 # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
106 # search_result = imap.search(["BODY", "hello"])
107 # fetch_result = fetch_thread.value
108 # imap.disconnect
109 #
110 # This script invokes the FETCH command and the SEARCH command concurrently.
111 #
112 # == Errors
113 #
114 # An IMAP server can send three different types of responses to indicate
115 # failure:
116 #
117 # NO:: the attempted command could not be successfully completed. For
118 # instance, the username/password used for logging in are incorrect;
119 # the selected mailbox does not exists; etc.
120 #
121 # BAD:: the request from the client does not follow the server's
122 # understanding of the IMAP protocol. This includes attempting
123 # commands from the wrong client state; for instance, attempting
124 # to perform a SEARCH command without having SELECTed a current
125 # mailbox. It can also signal an internal server
126 # failure (such as a disk crash) has occurred.
127 #
128 # BYE:: the server is saying goodbye. This can be part of a normal
129 # logout sequence, and can be used as part of a login sequence
130 # to indicate that the server is (for some reason) unwilling
131 # to accept our connection. As a response to any other command,
132 # it indicates either that the server is shutting down, or that
133 # the server is timing out the client connection due to inactivity.
134 #
135 # These three error response are represented by the errors
136 # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and
137 # Net::IMAP::ByeResponseError, all of which are subclasses of
138 # Net::IMAP::ResponseError. Essentially, all methods that involve
139 # sending a request to the server can generate one of these errors.
140 # Only the most pertinent instances have been documented below.
141 #
142 # Because the IMAP class uses Sockets for communication, its methods
143 # are also susceptible to the various errors that can occur when
144 # working with sockets. These are generally represented as
145 # Errno errors. For instance, any method that involves sending a
146 # request to the server and/or receiving a response from it could
147 # raise an Errno::EPIPE error if the network connection unexpectedly
148 # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2),
149 # and associated man pages.
150 #
151 # Finally, a Net::IMAP::DataFormatError is thrown if low-level data
152 # is found to be in an incorrect format (for instance, when converting
153 # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is
154 # thrown if a server response is non-parseable.
155 #
156 #
157 # == References
158 #
159 # [[IMAP]]
160 # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
161 # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501)
162 #
163 # [[LANGUAGE-TAGS]]
164 # Alvestrand, H., "Tags for the Identification of
165 # Languages", RFC 1766, March 1995.
166 #
167 # [[MD5]]
168 # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
169 # 1864, October 1995.
170 #
171 # [[MIME-IMB]]
172 # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
173 # Mail Extensions) Part One: Format of Internet Message Bodies", RFC
174 # 2045, November 1996.
175 #
176 # [[RFC-822]]
177 # Crocker, D., "Standard for the Format of ARPA Internet Text
178 # Messages", STD 11, RFC 822, University of Delaware, August 1982.
179 #
180 # [[RFC-2087]]
181 # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
182 #
183 # [[RFC-2086]]
184 # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
185 #
186 # [[RFC-2195]]
187 # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension
188 # for Simple Challenge/Response", RFC 2195, September 1997.
189 #
190 # [[SORT-THREAD-EXT]]
191 # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD
192 # Extensions", draft-ietf-imapext-sort, May 2003.
193 #
194 # [[OSSL]]
195 # http://www.openssl.org
196 #
197 # [[RSSL]]
198 # http://savannah.gnu.org/projects/rubypki
199 #
200 # [[UTF7]]
201 # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
202 # Unicode", RFC 2152, May 1997.
203 #
204 class IMAP
205 include MonitorMixin
206 if defined?(OpenSSL)
207 include OpenSSL
208 include SSL
209 end
210
211 # Returns an initial greeting response from the server.
212 attr_reader :greeting
213
214 # Returns recorded untagged responses. For example:
215 #
216 # imap.select("inbox")
217 # p imap.responses["EXISTS"][-1]
218 # #=> 2
219 # p imap.responses["UIDVALIDITY"][-1]
220 # #=> 968263756
221 attr_reader :responses
222
223 # Returns all response handlers.
224 attr_reader :response_handlers
225
226 # The thread to receive exceptions.
227 attr_accessor :client_thread
228
229 # Flag indicating a message has been seen
230 SEEN = :Seen
231
232 # Flag indicating a message has been answered
233 ANSWERED = :Answered
234
235 # Flag indicating a message has been flagged for special or urgent
236 # attention
237 FLAGGED = :Flagged
238
239 # Flag indicating a message has been marked for deletion. This
240 # will occur when the mailbox is closed or expunged.
241 DELETED = :Deleted
242
243 # Flag indicating a message is only a draft or work-in-progress version.
244 DRAFT = :Draft
245
246 # Flag indicating that the message is "recent", meaning that this
247 # session is the first session in which the client has been notified
248 # of this message.
249 RECENT = :Recent
250
251 # Flag indicating that a mailbox context name cannot contain
252 # children.
253 NOINFERIORS = :Noinferiors
254
255 # Flag indicating that a mailbox is not selected.
256 NOSELECT = :Noselect
257
258 # Flag indicating that a mailbox has been marked "interesting" by
259 # the server; this commonly indicates that the mailbox contains
260 # new messages.
261 MARKED = :Marked
262
263 # Flag indicating that the mailbox does not contains new messages.
264 UNMARKED = :Unmarked
265
266 # Returns the debug mode.
267 def self.debug
268 return @@debug
269 end
270
271 # Sets the debug mode.
272 def self.debug=(val)
273 return @@debug = val
274 end
275
276 # Returns the max number of flags interned to symbols.
277 def self.max_flag_count
278 return @@max_flag_count
279 end
280
281 # Sets the max number of flags interned to symbols.
282 def self.max_flag_count=(count)
283 @@max_flag_count = count
284 end
285
286 # Adds an authenticator for Net::IMAP#authenticate. +auth_type+
287 # is the type of authentication this authenticator supports
288 # (for instance, "LOGIN"). The +authenticator+ is an object
289 # which defines a process() method to handle authentication with
290 # the server. See Net::IMAP::LoginAuthenticator,
291 # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
292 # for examples.
293 #
294 #
295 # If +auth_type+ refers to an existing authenticator, it will be
296 # replaced by the new one.
297 def self.add_authenticator(auth_type, authenticator)
298 @@authenticators[auth_type] = authenticator
299 end
300
301 # Disconnects from the server.
302 def disconnect
303 begin
304 begin
305 # try to call SSL::SSLSocket#io.
306 @sock.io.shutdown
307 rescue NoMethodError
308 # @sock is not an SSL::SSLSocket.
309 @sock.shutdown
310 end
311 rescue Errno::ENOTCONN
312 # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
313 end
314 @receiver_thread.join
315 @sock.close
316 end
317
318 # Returns true if disconnected from the server.
319 def disconnected?
320 return @sock.closed?
321 end
322
323 # Sends a CAPABILITY command, and returns an array of
324 # capabilities that the server supports. Each capability
325 # is a string. See [IMAP] for a list of possible
326 # capabilities.
327 #
328 # Note that the Net::IMAP class does not modify its
329 # behaviour according to the capabilities of the server;
330 # it is up to the user of the class to ensure that
331 # a certain capability is supported by a server before
332 # using it.
333 def capability
334 synchronize do
335 send_command("CAPABILITY")
336 return @responses.delete("CAPABILITY")[-1]
337 end
338 end
339
340 # Sends a NOOP command to the server. It does nothing.
341 def noop
342 send_command("NOOP")
343 end
344
345 # Sends a LOGOUT command to inform the server that the client is
346 # done with the connection.
347 def logout
348 send_command("LOGOUT")
349 end
350
351 # Sends a STARTTLS command to start TLS session.
352 def starttls(options = {}, verify = true)
353 send_command("STARTTLS") do |resp|
354 if resp.kind_of?(TaggedResponse) && resp.name == "OK"
355 begin
356 # for backward compatibility
357 certs = options.to_str
358 options = create_ssl_params(certs, verify)
359 rescue NoMethodError
360 end
361 start_tls_session(options)
362 end
363 end
364 end
365
366 # Sends an AUTHENTICATE command to authenticate the client.
367 # The +auth_type+ parameter is a string that represents
368 # the authentication mechanism to be used. Currently Net::IMAP
369 # supports authentication mechanisms:
370 #
371 # LOGIN:: login using cleartext user and password.
372 # CRAM-MD5:: login with cleartext user and encrypted password
373 # (see [RFC-2195] for a full description). This
374 # mechanism requires that the server have the user's
375 # password stored in clear-text password.
376 #
377 # For both these mechanisms, there should be two +args+: username
378 # and (cleartext) password. A server may not support one or other
379 # of these mechanisms; check #capability() for a capability of
380 # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
381 #
382 # Authentication is done using the appropriate authenticator object:
383 # see @@authenticators for more information on plugging in your own
384 # authenticator.
385 #
386 # For example:
387 #
388 # imap.authenticate('LOGIN', user, password)
389 #
390 # A Net::IMAP::NoResponseError is raised if authentication fails.
391 def authenticate(auth_type, *args)
392 auth_type = auth_type.upcase
393 unless @@authenticators.has_key?(auth_type)
394 raise ArgumentError,
395 format('unknown auth type - "%s"', auth_type)
396 end
397 authenticator = @@authenticators[auth_type].new(*args)
398 send_command("AUTHENTICATE", auth_type) do |resp|
399 if resp.instance_of?(ContinuationRequest)
400 data = authenticator.process(resp.data.text.unpack("m")[0])
401 s = [data].pack("m").gsub(/\n/, "")
402 send_string_data(s)
403 put_string(CRLF)
404 end
405 end
406 end
407
408 # Sends a LOGIN command to identify the client and carries
409 # the plaintext +password+ authenticating this +user+. Note
410 # that, unlike calling #authenticate() with an +auth_type+
411 # of "LOGIN", #login() does *not* use the login authenticator.
412 #
413 # A Net::IMAP::NoResponseError is raised if authentication fails.
414 def login(user, password)
415 send_command("LOGIN", user, password)
416 end
417
418 # Sends a SELECT command to select a +mailbox+ so that messages
419 # in the +mailbox+ can be accessed.
420 #
421 # After you have selected a mailbox, you may retrieve the
422 # number of items in that mailbox from @responses["EXISTS"][-1],
423 # and the number of recent messages from @responses["RECENT"][-1].
424 # Note that these values can change if new messages arrive
425 # during a session; see #add_response_handler() for a way of
426 # detecting this event.
427 #
428 # A Net::IMAP::NoResponseError is raised if the mailbox does not
429 # exist or is for some reason non-selectable.
430 def select(mailbox)
431 synchronize do
432 @responses.clear
433 send_command("SELECT", mailbox)
434 end
435 end
436
437 # Sends a EXAMINE command to select a +mailbox+ so that messages
438 # in the +mailbox+ can be accessed. Behaves the same as #select(),
439 # except that the selected +mailbox+ is identified as read-only.
440 #
441 # A Net::IMAP::NoResponseError is raised if the mailbox does not
442 # exist or is for some reason non-examinable.
443 def examine(mailbox)
444 synchronize do
445 @responses.clear
446 send_command("EXAMINE", mailbox)
447 end
448 end
449
450 # Sends a CREATE command to create a new +mailbox+.
451 #
452 # A Net::IMAP::NoResponseError is raised if a mailbox with that name
453 # cannot be created.
454 def create(mailbox)
455 send_command("CREATE", mailbox)
456 end
457
458 # Sends a DELETE command to remove the +mailbox+.
459 #
460 # A Net::IMAP::NoResponseError is raised if a mailbox with that name
461 # cannot be deleted, either because it does not exist or because the
462 # client does not have permission to delete it.
463 def delete(mailbox)
464 send_command("DELETE", mailbox)
465 end
466
467 # Sends a RENAME command to change the name of the +mailbox+ to
468 # +newname+.
469 #
470 # A Net::IMAP::NoResponseError is raised if a mailbox with the
471 # name +mailbox+ cannot be renamed to +newname+ for whatever
472 # reason; for instance, because +mailbox+ does not exist, or
473 # because there is already a mailbox with the name +newname+.
474 def rename(mailbox, newname)
475 send_command("RENAME", mailbox, newname)
476 end
477
478 # Sends a SUBSCRIBE command to add the specified +mailbox+ name to
479 # the server's set of "active" or "subscribed" mailboxes as returned
480 # by #lsub().
481 #
482 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
483 # subscribed to, for instance because it does not exist.
484 def subscribe(mailbox)
485 send_command("SUBSCRIBE", mailbox)
486 end
487
488 # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name
489 # from the server's set of "active" or "subscribed" mailboxes.
490 #
491 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
492 # unsubscribed from, for instance because the client is not currently
493 # subscribed to it.
494 def unsubscribe(mailbox)
495 send_command("UNSUBSCRIBE", mailbox)
496 end
497
498 # Sends a LIST command, and returns a subset of names from
499 # the complete set of all names available to the client.
500 # +refname+ provides a context (for instance, a base directory
501 # in a directory-based mailbox hierarchy). +mailbox+ specifies
502 # a mailbox or (via wildcards) mailboxes under that context.
503 # Two wildcards may be used in +mailbox+: '*', which matches
504 # all characters *including* the hierarchy delimiter (for instance,
505 # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%',
506 # which matches all characters *except* the hierarchy delimiter.
507 #
508 # If +refname+ is empty, +mailbox+ is used directly to determine
509 # which mailboxes to match. If +mailbox+ is empty, the root
510 # name of +refname+ and the hierarchy delimiter are returned.
511 #
512 # The return value is an array of +Net::IMAP::MailboxList+. For example:
513 #
514 # imap.create("foo/bar")
515 # imap.create("foo/baz")
516 # p imap.list("", "foo/%")
517 # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
518 # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
519 # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
520 def list(refname, mailbox)
521 synchronize do
522 send_command("LIST", refname, mailbox)
523 return @responses.delete("LIST")
524 end
525 end
526
527 # Sends the GETQUOTAROOT command along with specified +mailbox+.
528 # This command is generally available to both admin and user.
529 # If mailbox exists, returns an array containing objects of
530 # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota.
531 def getquotaroot(mailbox)
532 synchronize do
533 send_command("GETQUOTAROOT", mailbox)
534 result = []
535 result.concat(@responses.delete("QUOTAROOT"))
536 result.concat(@responses.delete("QUOTA"))
537 return result
538 end
539 end
540
541 # Sends the GETQUOTA command along with specified +mailbox+.
542 # If this mailbox exists, then an array containing a
543 # Net::IMAP::MailboxQuota object is returned. This
544 # command generally is only available to server admin.
545 def getquota(mailbox)
546 synchronize do
547 send_command("GETQUOTA", mailbox)
548 return @responses.delete("QUOTA")
549 end
550 end
551
552 # Sends a SETQUOTA command along with the specified +mailbox+ and
553 # +quota+. If +quota+ is nil, then quota will be unset for that
554 # mailbox. Typically one needs to be logged in as server admin
555 # for this to work. The IMAP quota commands are described in
556 # [RFC-2087].
557 def setquota(mailbox, quota)
558 if quota.nil?
559 data = '()'
560 else
561 data = '(STORAGE ' + quota.to_s + ')'
562 end
563 send_command("SETQUOTA", mailbox, RawData.new(data))
564 end
565
566 # Sends the SETACL command along with +mailbox+, +user+ and the
567 # +rights+ that user is to have on that mailbox. If +rights+ is nil,
568 # then that user will be stripped of any rights to that mailbox.
569 # The IMAP ACL commands are described in [RFC-2086].
570 def setacl(mailbox, user, rights)
571 if rights.nil?
572 send_command("SETACL", mailbox, user, "")
573 else
574 send_command("SETACL", mailbox, user, rights)
575 end
576 end
577
578 # Send the GETACL command along with specified +mailbox+.
579 # If this mailbox exists, an array containing objects of
580 # Net::IMAP::MailboxACLItem will be returned.
581 def getacl(mailbox)
582 synchronize do
583 send_command("GETACL", mailbox)
584 return @responses.delete("ACL")[-1]
585 end
586 end
587
588 # Sends a LSUB command, and returns a subset of names from the set
589 # of names that the user has declared as being "active" or
590 # "subscribed". +refname+ and +mailbox+ are interpreted as
591 # for #list().
592 # The return value is an array of +Net::IMAP::MailboxList+.
593 def lsub(refname, mailbox)
594 synchronize do
595 send_command("LSUB", refname, mailbox)
596 return @responses.delete("LSUB")
597 end
598 end
599
600 # Sends a STATUS command, and returns the status of the indicated
601 # +mailbox+. +attr+ is a list of one or more attributes that
602 # we are request the status of. Supported attributes include:
603 #
604 # MESSAGES:: the number of messages in the mailbox.
605 # RECENT:: the number of recent messages in the mailbox.
606 # UNSEEN:: the number of unseen messages in the mailbox.
607 #
608 # The return value is a hash of attributes. For example:
609 #
610 # p imap.status("inbox", ["MESSAGES", "RECENT"])
611 # #=> {"RECENT"=>0, "MESSAGES"=>44}
612 #
613 # A Net::IMAP::NoResponseError is raised if status values
614 # for +mailbox+ cannot be returned, for instance because it
615 # does not exist.
616 def status(mailbox, attr)
617 synchronize do
618 send_command("STATUS", mailbox, attr)
619 return @responses.delete("STATUS")[-1].attr
620 end
621 end
622
623 # Sends a APPEND command to append the +message+ to the end of
624 # the +mailbox+. The optional +flags+ argument is an array of
625 # flags to initially passing to the new message. The optional
626 # +date_time+ argument specifies the creation time to assign to the
627 # new message; it defaults to the current time.
628 # For example:
629 #
630 # imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
631 # Subject: hello
632 # From: shugo@ruby-lang.org
633 # To: shugo@ruby-lang.org
634 #
635 # hello world
636 # EOF
637 #
638 # A Net::IMAP::NoResponseError is raised if the mailbox does
639 # not exist (it is not created automatically), or if the flags,
640 # date_time, or message arguments contain errors.
641 def append(mailbox, message, flags = nil, date_time = nil)
642 args = []
643 if flags
644 args.push(flags)
645 end
646 args.push(date_time) if date_time
647 args.push(Literal.new(message))
648 send_command("APPEND", mailbox, *args)
649 end
650
651 # Sends a CHECK command to request a checkpoint of the currently
652 # selected mailbox. This performs implementation-specific
653 # housekeeping, for instance, reconciling the mailbox's
654 # in-memory and on-disk state.
655 def check
656 send_command("CHECK")
657 end
658
659 # Sends a CLOSE command to close the currently selected mailbox.
660 # The CLOSE command permanently removes from the mailbox all
661 # messages that have the \Deleted flag set.
662 def close
663 send_command("CLOSE")
664 end
665
666 # Sends a EXPUNGE command to permanently remove from the currently
667 # selected mailbox all messages that have the \Deleted flag set.
668 def expunge
669 synchronize do
670 send_command("EXPUNGE")
671 return @responses.delete("EXPUNGE")
672 end
673 end
674
675 # Sends a SEARCH command to search the mailbox for messages that
676 # match the given searching criteria, and returns message sequence
677 # numbers. +keys+ can either be a string holding the entire
678 # search string, or a single-dimension array of search keywords and
679 # arguments. The following are some common search criteria;
680 # see [IMAP] section 6.4.4 for a full list.
681 #
682 # <message set>:: a set of message sequence numbers. ',' indicates
683 # an interval, ':' indicates a range. For instance,
684 # '2,10:12,15' means "2,10,11,12,15".
685 #
686 # BEFORE <date>:: messages with an internal date strictly before
687 # <date>. The date argument has a format similar
688 # to 8-Aug-2002.
689 #
690 # BODY <string>:: messages that contain <string> within their body.
691 #
692 # CC <string>:: messages containing <string> in their CC field.
693 #
694 # FROM <string>:: messages that contain <string> in their FROM field.
695 #
696 # NEW:: messages with the \Recent, but not the \Seen, flag set.
697 #
698 # NOT <search-key>:: negate the following search key.
699 #
700 # OR <search-key> <search-key>:: "or" two search keys together.
701 #
702 # ON <date>:: messages with an internal date exactly equal to <date>,
703 # which has a format similar to 8-Aug-2002.
704 #
705 # SINCE <date>:: messages with an internal date on or after <date>.
706 #
707 # SUBJECT <string>:: messages with <string> in their subject.
708 #
709 # TO <string>:: messages with <string> in their TO field.
710 #
711 # For example:
712 #
713 # p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
714 # #=> [1, 6, 7, 8]
715 def search(keys, charset = nil)
716 return search_internal("SEARCH", keys, charset)
717 end
718
719 # As for #search(), but returns unique identifiers.
720 def uid_search(keys, charset = nil)
721 return search_internal("UID SEARCH", keys, charset)
722 end
723
724 # Sends a FETCH command to retrieve data associated with a message
725 # in the mailbox. The +set+ parameter is a number or an array of
726 # numbers or a Range object. The number is a message sequence
727 # number. +attr+ is a list of attributes to fetch; see the
728 # documentation for Net::IMAP::FetchData for a list of valid
729 # attributes.
730 # The return value is an array of Net::IMAP::FetchData. For example:
731 #
732 # p imap.fetch(6..8, "UID")
733 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\
734 # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\
735 # #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
736 # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
737 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
738 # data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0]
739 # p data.seqno
740 # #=> 6
741 # p data.attr["RFC822.SIZE"]
742 # #=> 611
743 # p data.attr["INTERNALDATE"]
744 # #=> "12-Oct-2000 22:40:59 +0900"
745 # p data.attr["UID"]
746 # #=> 98
747 def fetch(set, attr)
748 return fetch_internal("FETCH", set, attr)
749 end
750
751 # As for #fetch(), but +set+ contains unique identifiers.
752 def uid_fetch(set, attr)
753 return fetch_internal("UID FETCH", set, attr)
754 end
755
756 # Sends a STORE command to alter data associated with messages
757 # in the mailbox, in particular their flags. The +set+ parameter
758 # is a number or an array of numbers or a Range object. Each number
759 # is a message sequence number. +attr+ is the name of a data item
760 # to store: 'FLAGS' means to replace the message's flag list
761 # with the provided one; '+FLAGS' means to add the provided flags;
762 # and '-FLAGS' means to remove them. +flags+ is a list of flags.
763 #
764 # The return value is an array of Net::IMAP::FetchData. For example:
765 #
766 # p imap.store(6..8, "+FLAGS", [:Deleted])
767 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
768 # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
769 # #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
770 def store(set, attr, flags)
771 return store_internal("STORE", set, attr, flags)
772 end
773
774 # As for #store(), but +set+ contains unique identifiers.
775 def uid_store(set, attr, flags)
776 return store_internal("UID STORE", set, attr, flags)
777 end
778
779 # Sends a COPY command to copy the specified message(s) to the end
780 # of the specified destination +mailbox+. The +set+ parameter is
781 # a number or an array of numbers or a Range object. The number is
782 # a message sequence number.
783 def copy(set, mailbox)
784 copy_internal("COPY", set, mailbox)
785 end
786
787 # As for #copy(), but +set+ contains unique identifiers.
788 def uid_copy(set, mailbox)
789 copy_internal("UID COPY", set, mailbox)
790 end
791
792 # Sends a SORT command to sort messages in the mailbox.
793 # Returns an array of message sequence numbers. For example:
794 #
795 # p imap.sort(["FROM"], ["ALL"], "US-ASCII")
796 # #=> [1, 2, 3, 5, 6, 7, 8, 4, 9]
797 # p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII")
798 # #=> [6, 7, 8, 1]
799 #
800 # See [SORT-THREAD-EXT] for more details.
801 def sort(sort_keys, search_keys, charset)
802 return sort_internal("SORT", sort_keys, search_keys, charset)
803 end
804
805 # As for #sort(), but returns an array of unique identifiers.
806 def uid_sort(sort_keys, search_keys, charset)
807 return sort_internal("UID SORT", sort_keys, search_keys, charset)
808 end
809
810 # Adds a response handler. For example, to detect when
811 # the server sends us a new EXISTS response (which normally
812 # indicates new messages being added to the mail box),
813 # you could add the following handler after selecting the
814 # mailbox.
815 #
816 # imap.add_response_handler { |resp|
817 # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
818 # puts "Mailbox now has #{resp.data} messages"
819 # end
820 # }
821 #
822 def add_response_handler(handler = Proc.new)
823 @response_handlers.push(handler)
824 end
825
826 # Removes the response handler.
827 def remove_response_handler(handler)
828 @response_handlers.delete(handler)
829 end
830
831 # As for #search(), but returns message sequence numbers in threaded
832 # format, as a Net::IMAP::ThreadMember tree. The supported algorithms
833 # are:
834 #
835 # ORDEREDSUBJECT:: split into single-level threads according to subject,
836 # ordered by date.
837 # REFERENCES:: split into threads by parent/child relationships determined
838 # by which message is a reply to which.
839 #
840 # Unlike #search(), +charset+ is a required argument. US-ASCII
841 # and UTF-8 are sample values.
842 #
843 # See [SORT-THREAD-EXT] for more details.
844 def thread(algorithm, search_keys, charset)
845 return thread_internal("THREAD", algorithm, search_keys, charset)
846 end
847
848 # As for #thread(), but returns unique identifiers instead of
849 # message sequence numbers.
850 def uid_thread(algorithm, search_keys, charset)
851 return thread_internal("UID THREAD", algorithm, search_keys, charset)
852 end
853
854 # Sends an IDLE command that waits for notifications of new or expunged
855 # messages. Yields responses from the server during the IDLE.
856 #
857 # Use #idle_done() to leave IDLE.
858 def idle(&response_handler)
859 raise LocalJumpError, "no block given" unless response_handler
860
861 response = nil
862
863 synchronize do
864 tag = Thread.current[:net_imap_tag] = generate_tag
865 put_string("#{tag} IDLE#{CRLF}")
866
867 begin
868 add_response_handler(response_handler)
869 @idle_done_cond = new_cond
870 @idle_done_cond.wait
871 @idle_done_cond = nil
872 ensure
873 remove_response_handler(response_handler)
874 put_string("DONE#{CRLF}")
875 response = get_tagged_response(tag, "IDLE")
876 end
877 end
878
879 return response
880 end
881
882 # Leaves IDLE.
883 def idle_done
884 synchronize do
885 if @idle_done_cond.nil?
886 raise Net::IMAP::Error, "not during IDLE"
887 end
888 @idle_done_cond.signal
889 end
890 end
891
892 # Decode a string from modified UTF-7 format to UTF-8.
893 #
894 # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
895 # slightly modified version of this to encode mailbox names
896 # containing non-ASCII characters; see [IMAP] section 5.1.3.
897 #
898 # Net::IMAP does _not_ automatically encode and decode
899 # mailbox names to and from utf7.
900 def self.decode_utf7(s)
901 return s.gsub(/&(.*?)-/n) {
902 if $1.empty?
903 "&"
904 else
905 base64 = $1.tr(",", "/")
906 x = base64.length % 4
907 if x > 0
908 base64.concat("=" * (4 - x))
909 end
910 base64.unpack("m")[0].unpack("n*").pack("U*")
911 end
912 }.force_encoding("UTF-8")
913 end
914
915 # Encode a string from UTF-8 format to modified UTF-7.
916 def self.encode_utf7(s)
917 return s.gsub(/(&)|([^\x20-\x7e]+)/u) {
918 if $1
919 "&-"
920 else
921 base64 = [$&.unpack("U*").pack("n*")].pack("m")
922 "&" + base64.delete("=\n").tr("/", ",") + "-"
923 end
924 }.force_encoding("ASCII-8BIT")
925 end
926
927 # Formats +time+ as an IMAP-style date.
928 def self.format_date(time)
929 return time.strftime('%d-%b-%Y')
930 end
931
932 # Formats +time+ as an IMAP-style date-time.
933 def self.format_datetime(time)
934 return time.strftime('%d-%b-%Y %H:%M %z')
935 end
936
937 private
938
939 CRLF = "\r\n" # :nodoc:
940 PORT = 143 # :nodoc:
941 SSL_PORT = 993 # :nodoc:
942
943 @@debug = false
944 @@authenticators = {}
945 @@max_flag_count = 10000
946
947 # call-seq:
948 # Net::IMAP.new(host, options = {})
949 #
950 # Creates a new Net::IMAP object and connects it to the specified
951 # +host+.
952 #
953 # +options+ is an option hash, each key of which is a symbol.
954 #
955 # The available options are:
956 #
957 # port:: port number (default value is 143 for imap, or 993 for imaps)
958 # ssl:: if options[:ssl] is true, then an attempt will be made
959 # to use SSL (now TLS) to connect to the server. For this to work
960 # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to
961 # be installed.
962 # if options[:ssl] is a hash, it's passed to
963 # OpenSSL::SSL::SSLContext#set_params as parameters.
964 #
965 # The most common errors are:
966 #
967 # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening
968 # firewall.
969 # Errno::ETIMEDOUT:: connection timed out (possibly due to packets
970 # being dropped by an intervening firewall).
971 # Errno::ENETUNREACH:: there is no route to that network.
972 # SocketError:: hostname not known or other socket error.
973 # Net::IMAP::ByeResponseError:: we connected to the host, but they
974 # immediately said goodbye to us.
975 def initialize(host, port_or_options = {},
976 usessl = false, certs = nil, verify = true)
977 super()
978 @host = host
979 begin
980 options = port_or_options.to_hash
981 rescue NoMethodError
982 # for backward compatibility
983 options = {}
984 options[:port] = port_or_options
985 if usessl
986 options[:ssl] = create_ssl_params(certs, verify)
987 end
988 end
989 @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
990 @tag_prefix = "RUBY"
991 @tagno = 0
992 @parser = ResponseParser.new
993 @sock = TCPSocket.open(@host, @port)
994 if options[:ssl]
995 start_tls_session(options[:ssl])
996 @usessl = true
997 else
998 @usessl = false
999 end
1000 @responses = Hash.new([].freeze)
1001 @tagged_responses = {}
1002 @response_handlers = []
1003 @tagged_response_arrival = new_cond
1004 @continuation_request_arrival = new_cond
1005 @idle_done_cond = nil
1006 @logout_command_tag = nil
1007 @debug_output_bol = true
1008 @exception = nil
1009
1010 @greeting = get_response
1011 if @greeting.name == "BYE"
1012 @sock.close
1013 raise ByeResponseError, @greeting
1014 end
1015
1016 @client_thread = Thread.current
1017 @receiver_thread = Thread.start {
1018 receive_responses
1019 }
1020 end
1021
1022 def receive_responses
1023 connection_closed = false
1024 until connection_closed
1025 synchronize do
1026 @exception = nil
1027 end
1028 begin
1029 resp = get_response
1030 rescue Exception => e
1031 synchronize do
1032 @sock.close
1033 @exception = e
1034 end
1035 break
1036 end
1037 unless resp
1038 synchronize do
1039 @exception = EOFError.new("end of file reached")
1040 end
1041 break
1042 end
1043 begin
1044 synchronize do
1045 case resp
1046 when TaggedResponse
1047 @tagged_responses[resp.tag] = resp
1048 @tagged_response_arrival.broadcast
1049 if resp.tag == @logout_command_tag
1050 return
1051 end
1052 when UntaggedResponse
1053 record_response(resp.name, resp.data)
1054 if resp.data.instance_of?(ResponseText) &&
1055 (code = resp.data.code)
1056 record_response(code.name, code.data)
1057 end
1058 if resp.name == "BYE" && @logout_command_tag.nil?
1059 @sock.close
1060 @exception = ByeResponseError.new(resp)
1061 connection_closed = true
1062 end
1063 when ContinuationRequest
1064 @continuation_request_arrival.signal
1065 end
1066 @response_handlers.each do |handler|
1067 handler.call(resp)
1068 end
1069 end
1070 rescue Exception => e
1071 @exception = e
1072 synchronize do
1073 @tagged_response_arrival.broadcast
1074 @continuation_request_arrival.broadcast
1075 end
1076 end
1077 end
1078 synchronize do
1079 @tagged_response_arrival.broadcast
1080 @continuation_request_arrival.broadcast
1081 end
1082 end
1083
1084 def get_tagged_response(tag, cmd)
1085 until @tagged_responses.key?(tag)
1086 raise @exception if @exception
1087 @tagged_response_arrival.wait
1088 end
1089 resp = @tagged_responses.delete(tag)
1090 case resp.name
1091 when /\A(?:NO)\z/ni
1092 raise NoResponseError, resp
1093 when /\A(?:BAD)\z/ni
1094 raise BadResponseError, resp
1095 else
1096 return resp
1097 end
1098 end
1099
1100 def get_response
1101 buff = ""
1102 while true
1103 s = @sock.gets(CRLF)
1104 break unless s
1105 buff.concat(s)
1106 if /\{(\d+)\}\r\n/n =~ s
1107 s = @sock.read($1.to_i)
1108 buff.concat(s)
1109 else
1110 break
1111 end
1112 end
1113 return nil if buff.length == 0
1114 if @@debug
1115 $stderr.print(buff.gsub(/^/n, "S: "))
1116 end
1117 return @parser.parse(buff)
1118 end
1119
1120 def record_response(name, data)
1121 unless @responses.has_key?(name)
1122 @responses[name] = []
1123 end
1124 @responses[name].push(data)
1125 end
1126
1127 def send_command(cmd, *args, &block)
1128 synchronize do
1129 args.each do |i|
1130 validate_data(i)
1131 end
1132 tag = generate_tag
1133 put_string(tag + " " + cmd)
1134 args.each do |i|
1135 put_string(" ")
1136 send_data(i)
1137 end
1138 put_string(CRLF)
1139 if cmd == "LOGOUT"
1140 @logout_command_tag = tag
1141 end
1142 if block
1143 add_response_handler(block)
1144 end
1145 begin
1146 return get_tagged_response(tag, cmd)
1147 ensure
1148 if block
1149 remove_response_handler(block)
1150 end
1151 end
1152 end
1153 end
1154
1155 def generate_tag
1156 @tagno += 1
1157 return format("%s%04d", @tag_prefix, @tagno)
1158 end
1159
1160 def put_string(str)
1161 @sock.print(str)
1162 if @@debug
1163 if @debug_output_bol
1164 $stderr.print("C: ")
1165 end
1166 $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: "))
1167 if /\r\n\z/n.match(str)
1168 @debug_output_bol = true
1169 else
1170 @debug_output_bol = false
1171 end
1172 end
1173 end
1174
1175 def validate_data(data)
1176 case data
1177 when nil
1178 when String
1179 when Integer
1180 if data < 0 || data >= 4294967296
1181 raise DataFormatError, num.to_s
1182 end
1183 when Array
1184 data.each do |i|
1185 validate_data(i)
1186 end
1187 when Time
1188 when Symbol
1189 else
1190 data.validate
1191 end
1192 end
1193
1194 def send_data(data)
1195 case data
1196 when nil
1197 put_string("NIL")
1198 when String
1199 send_string_data(data)
1200 when Integer
1201 send_number_data(data)
1202 when Array
1203 send_list_data(data)
1204 when Time
1205 send_time_data(data)
1206 when Symbol
1207 send_symbol_data(data)
1208 else
1209 data.send_data(self)
1210 end
1211 end
1212
1213 def send_string_data(str)
1214 case str
1215 when ""
1216 put_string('""')
1217 when /[\x80-\xff\r\n]/n
1218 # literal
1219 send_literal(str)
1220 when /[(){ \x00-\x1f\x7f%*"\\]/n
1221 # quoted string
1222 send_quoted_string(str)
1223 else
1224 put_string(str)
1225 end
1226 end
1227
1228 def send_quoted_string(str)
1229 put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
1230 end
1231
1232 def send_literal(str)
1233 put_string("{" + str.length.to_s + "}" + CRLF)
1234 @continuation_request_arrival.wait
1235 raise @exception if @exception
1236 put_string(str)
1237 end
1238
1239 def send_number_data(num)
1240 put_string(num.to_s)
1241 end
1242
1243 def send_list_data(list)
1244 put_string("(")
1245 first = true
1246 list.each do |i|
1247 if first
1248 first = false
1249 else
1250 put_string(" ")
1251 end
1252 send_data(i)
1253 end
1254 put_string(")")
1255 end
1256
1257 DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
1258
1259 def send_time_data(time)
1260 t = time.dup.gmtime
1261 s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
1262 t.day, DATE_MONTH[t.month - 1], t.year,
1263 t.hour, t.min, t.sec)
1264 put_string(s)
1265 end
1266
1267 def send_symbol_data(symbol)
1268 put_string("\\" + symbol.to_s)
1269 end
1270
1271 def search_internal(cmd, keys, charset)
1272 if keys.instance_of?(String)
1273 keys = [RawData.new(keys)]
1274 else
1275 normalize_searching_criteria(keys)
1276 end
1277 synchronize do
1278 if charset
1279 send_command(cmd, "CHARSET", charset, *keys)
1280 else
1281 send_command(cmd, *keys)
1282 end
1283 return @responses.delete("SEARCH")[-1]
1284 end
1285 end
1286
1287 def fetch_internal(cmd, set, attr)
1288 if attr.instance_of?(String)
1289 attr = RawData.new(attr)
1290 end
1291 synchronize do
1292 @responses.delete("FETCH")
1293 send_command(cmd, MessageSet.new(set), attr)
1294 return @responses.delete("FETCH")
1295 end
1296 end
1297
1298 def store_internal(cmd, set, attr, flags)
1299 if attr.instance_of?(String)
1300 attr = RawData.new(attr)
1301 end
1302 synchronize do
1303 @responses.delete("FETCH")
1304 send_command(cmd, MessageSet.new(set), attr, flags)
1305 return @responses.delete("FETCH")
1306 end
1307 end
1308
1309 def copy_internal(cmd, set, mailbox)
1310 send_command(cmd, MessageSet.new(set), mailbox)
1311 end
1312
1313 def sort_internal(cmd, sort_keys, search_keys, charset)
1314 if search_keys.instance_of?(String)
1315 search_keys = [RawData.new(search_keys)]
1316 else
1317 normalize_searching_criteria(search_keys)
1318 end
1319 normalize_searching_criteria(search_keys)
1320 synchronize do
1321 send_command(cmd, sort_keys, charset, *search_keys)
1322 return @responses.delete("SORT")[-1]
1323 end
1324 end
1325
1326 def thread_internal(cmd, algorithm, search_keys, charset)
1327 if search_keys.instance_of?(String)
1328 search_keys = [RawData.new(search_keys)]
1329 else
1330 normalize_searching_criteria(search_keys)
1331 end
1332 normalize_searching_criteria(search_keys)
1333 send_command(cmd, algorithm, charset, *search_keys)
1334 return @responses.delete("THREAD")[-1]
1335 end
1336
1337 def normalize_searching_criteria(keys)
1338 keys.collect! do |i|
1339 case i
1340 when -1, Range, Array
1341 MessageSet.new(i)
1342 else
1343 i
1344 end
1345 end
1346 end
1347
1348 def create_ssl_params(certs = nil, verify = true)
1349 params = {}
1350 if certs
1351 if File.file?(certs)
1352 params[:ca_file] = certs
1353 elsif File.directory?(certs)
1354 params[:ca_path] = certs
1355 end
1356 end
1357 if verify
1358 params[:verify_mode] = VERIFY_PEER
1359 else
1360 params[:verify_mode] = VERIFY_NONE
1361 end
1362 return params
1363 end
1364
1365 def start_tls_session(params = {})
1366 unless defined?(OpenSSL)
1367 raise "SSL extension not installed"
1368 end
1369 if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
1370 raise RuntimeError, "already using SSL"
1371 end
1372 begin
1373 params = params.to_hash
1374 rescue NoMethodError
1375 params = {}
1376 end
1377 context = SSLContext.new
1378 context.set_params(params)
1379 if defined?(VerifyCallbackProc)
1380 context.verify_callback = VerifyCallbackProc
1381 end
1382 @sock = SSLSocket.new(@sock, context)
1383 @sock.sync_close = true
1384 @sock.connect
1385 if context.verify_mode != VERIFY_NONE
1386 @sock.post_connection_check(@host)
1387 end
1388 end
1389
1390 class RawData # :nodoc:
1391 def send_data(imap)
1392 imap.send(:put_string, @data)
1393 end
1394
1395 def validate
1396 end
1397
1398 private
1399
1400 def initialize(data)
1401 @data = data
1402 end
1403 end
1404
1405 class Atom # :nodoc:
1406 def send_data(imap)
1407 imap.send(:put_string, @data)
1408 end
1409
1410 def validate
1411 end
1412
1413 private
1414
1415 def initialize(data)
1416 @data = data
1417 end
1418 end
1419
1420 class QuotedString # :nodoc:
1421 def send_data(imap)
1422 imap.send(:send_quoted_string, @data)
1423 end
1424
1425 def validate
1426 end
1427
1428 private
1429
1430 def initialize(data)
1431 @data = data
1432 end
1433 end
1434
1435 class Literal # :nodoc:
1436 def send_data(imap)
1437 imap.send(:send_literal, @data)
1438 end
1439
1440 def validate
1441 end
1442
1443 private
1444
1445 def initialize(data)
1446 @data = data
1447 end
1448 end
1449
1450 class MessageSet # :nodoc:
1451 def send_data(imap)
1452 imap.send(:put_string, format_internal(@data))
1453 end
1454
1455 def validate
1456 validate_internal(@data)
1457 end
1458
1459 private
1460
1461 def initialize(data)
1462 @data = data
1463 end
1464
1465 def format_internal(data)
1466 case data
1467 when "*"
1468 return data
1469 when Integer
1470 if data == -1
1471 return "*"
1472 else
1473 return data.to_s
1474 end
1475 when Range
1476 return format_internal(data.first) +
1477 ":" + format_internal(data.last)
1478 when Array
1479 return data.collect {|i| format_internal(i)}.join(",")
1480 when ThreadMember
1481 return data.seqno.to_s +
1482 ":" + data.children.collect {|i| format_internal(i).join(",")}
1483 end
1484 end
1485
1486 def validate_internal(data)
1487 case data
1488 when "*"
1489 when Integer
1490 ensure_nz_number(data)
1491 when Range
1492 when Array
1493 data.each do |i|
1494 validate_internal(i)
1495 end
1496 when ThreadMember
1497 data.children.each do |i|
1498 validate_internal(i)
1499 end
1500 else
1501 raise DataFormatError, data.inspect
1502 end
1503 end
1504
1505 def ensure_nz_number(num)
1506 if num < -1 || num == 0 || num >= 4294967296
1507 msg = "nz_number must be non-zero unsigned 32-bit integer: " +
1508 num.inspect
1509 raise DataFormatError, msg
1510 end
1511 end
1512 end
1513
1514 # Net::IMAP::ContinuationRequest represents command continuation requests.
1515 #
1516 # The command continuation request response is indicated by a "+" token
1517 # instead of a tag. This form of response indicates that the server is
1518 # ready to accept the continuation of a command from the client. The
1519 # remainder of this response is a line of text.
1520 #
1521 # continue_req ::= "+" SPACE (resp_text / base64)
1522 #
1523 # ==== Fields:
1524 #
1525 # data:: Returns the data (Net::IMAP::ResponseText).
1526 #
1527 # raw_data:: Returns the raw data string.
1528 ContinuationRequest = Struct.new(:data, :raw_data)
1529
1530 # Net::IMAP::UntaggedResponse represents untagged responses.
1531 #
1532 # Data transmitted by the server to the client and status responses
1533 # that do not indicate command completion are prefixed with the token
1534 # "*", and are called untagged responses.
1535 #
1536 # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
1537 # mailbox_data / message_data / capability_data)
1538 #
1539 # ==== Fields:
1540 #
1541 # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
1542 #
1543 # data:: Returns the data such as an array of flag symbols,
1544 # a ((<Net::IMAP::MailboxList>)) object....
1545 #
1546 # raw_data:: Returns the raw data string.
1547 UntaggedResponse = Struct.new(:name, :data, :raw_data)
1548
1549 # Net::IMAP::TaggedResponse represents tagged responses.
1550 #
1551 # The server completion result response indicates the success or
1552 # failure of the operation. It is tagged with the same tag as the
1553 # client command which began the operation.
1554 #
1555 # response_tagged ::= tag SPACE resp_cond_state CRLF
1556 #
1557 # tag ::= 1*<any ATOM_CHAR except "+">
1558 #
1559 # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
1560 #
1561 # ==== Fields:
1562 #
1563 # tag:: Returns the tag.
1564 #
1565 # name:: Returns the name. the name is one of "OK", "NO", "BAD".
1566 #
1567 # data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
1568 #
1569 # raw_data:: Returns the raw data string.
1570 #
1571 TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
1572
1573 # Net::IMAP::ResponseText represents texts of responses.
1574 # The text may be prefixed by the response code.
1575 #
1576 # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
1577 # ;; text SHOULD NOT begin with "[" or "="
1578 #
1579 # ==== Fields:
1580 #
1581 # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
1582 #
1583 # text:: Returns the text.
1584 #
1585 ResponseText = Struct.new(:code, :text)
1586
1587 #
1588 # Net::IMAP::ResponseCode represents response codes.
1589 #
1590 # resp_text_code ::= "ALERT" / "PARSE" /
1591 # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
1592 # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1593 # "UIDVALIDITY" SPACE nz_number /
1594 # "UNSEEN" SPACE nz_number /
1595 # atom [SPACE 1*<any TEXT_CHAR except "]">]
1596 #
1597 # ==== Fields:
1598 #
1599 # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
1600 #
1601 # data:: Returns the data if it exists.
1602 #
1603 ResponseCode = Struct.new(:name, :data)
1604
1605 # Net::IMAP::MailboxList represents contents of the LIST response.
1606 #
1607 # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
1608 # "\Noselect" / "\Unmarked" / flag_extension) ")"
1609 # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
1610 #
1611 # ==== Fields:
1612 #
1613 # attr:: Returns the name attributes. Each name attribute is a symbol
1614 # capitalized by String#capitalize, such as :Noselect (not :NoSelect).
1615 #
1616 # delim:: Returns the hierarchy delimiter
1617 #
1618 # name:: Returns the mailbox name.
1619 #
1620 MailboxList = Struct.new(:attr, :delim, :name)
1621
1622 # Net::IMAP::MailboxQuota represents contents of GETQUOTA response.
1623 # This object can also be a response to GETQUOTAROOT. In the syntax
1624 # specification below, the delimiter used with the "#" construct is a
1625 # single space (SPACE).
1626 #
1627 # quota_list ::= "(" #quota_resource ")"
1628 #
1629 # quota_resource ::= atom SPACE number SPACE number
1630 #
1631 # quota_response ::= "QUOTA" SPACE astring SPACE quota_list
1632 #
1633 # ==== Fields:
1634 #
1635 # mailbox:: The mailbox with the associated quota.
1636 #
1637 # usage:: Current storage usage of mailbox.
1638 #
1639 # quota:: Quota limit imposed on mailbox.
1640 #
1641 MailboxQuota = Struct.new(:mailbox, :usage, :quota)
1642
1643 # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
1644 # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
1645 #
1646 # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
1647 #
1648 # ==== Fields:
1649 #
1650 # mailbox:: The mailbox with the associated quota.
1651 #
1652 # quotaroots:: Zero or more quotaroots that effect the quota on the
1653 # specified mailbox.
1654 #
1655 MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
1656
1657 # Net::IMAP::MailboxACLItem represents response from GETACL.
1658 #
1659 # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
1660 #
1661 # identifier ::= astring
1662 #
1663 # rights ::= astring
1664 #
1665 # ==== Fields:
1666 #
1667 # user:: Login name that has certain rights to the mailbox
1668 # that was specified with the getacl command.
1669 #
1670 # rights:: The access rights the indicated user has to the
1671 # mailbox.
1672 #
1673 MailboxACLItem = Struct.new(:user, :rights)
1674
1675 # Net::IMAP::StatusData represents contents of the STATUS response.
1676 #
1677 # ==== Fields:
1678 #
1679 # mailbox:: Returns the mailbox name.
1680 #
1681 # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
1682 # "UIDVALIDITY", "UNSEEN". Each value is a number.
1683 #
1684 StatusData = Struct.new(:mailbox, :attr)
1685
1686 # Net::IMAP::FetchData represents contents of the FETCH response.
1687 #
1688 # ==== Fields:
1689 #
1690 # seqno:: Returns the message sequence number.
1691 # (Note: not the unique identifier, even for the UID command response.)
1692 #
1693 # attr:: Returns a hash. Each key is a data item name, and each value is
1694 # its value.
1695 #
1696 # The current data items are:
1697 #
1698 # [BODY]
1699 # A form of BODYSTRUCTURE without extension data.
1700 # [BODY[<section>]<<origin_octet>>]
1701 # A string expressing the body contents of the specified section.
1702 # [BODYSTRUCTURE]
1703 # An object that describes the [MIME-IMB] body structure of a message.
1704 # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText,
1705 # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart.
1706 # [ENVELOPE]
1707 # A Net::IMAP::Envelope object that describes the envelope
1708 # structure of a message.
1709 # [FLAGS]
1710 # A array of flag symbols that are set for this message. flag symbols
1711 # are capitalized by String#capitalize.
1712 # [INTERNALDATE]
1713 # A string representing the internal date of the message.
1714 # [RFC822]
1715 # Equivalent to BODY[].
1716 # [RFC822.HEADER]
1717 # Equivalent to BODY.PEEK[HEADER].
1718 # [RFC822.SIZE]
1719 # A number expressing the [RFC-822] size of the message.
1720 # [RFC822.TEXT]
1721 # Equivalent to BODY[TEXT].
1722 # [UID]
1723 # A number expressing the unique identifier of the message.
1724 #
1725 FetchData = Struct.new(:seqno, :attr)
1726
1727 # Net::IMAP::Envelope represents envelope structures of messages.
1728 #
1729 # ==== Fields:
1730 #
1731 # date:: Returns a string that represents the date.
1732 #
1733 # subject:: Returns a string that represents the subject.
1734 #
1735 # from:: Returns an array of Net::IMAP::Address that represents the from.
1736 #
1737 # sender:: Returns an array of Net::IMAP::Address that represents the sender.
1738 #
1739 # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to.
1740 #
1741 # to:: Returns an array of Net::IMAP::Address that represents the to.
1742 #
1743 # cc:: Returns an array of Net::IMAP::Address that represents the cc.
1744 #
1745 # bcc:: Returns an array of Net::IMAP::Address that represents the bcc.
1746 #
1747 # in_reply_to:: Returns a string that represents the in-reply-to.
1748 #
1749 # message_id:: Returns a string that represents the message-id.
1750 #
1751 Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
1752 :to, :cc, :bcc, :in_reply_to, :message_id)
1753
1754 #
1755 # Net::IMAP::Address represents electronic mail addresses.
1756 #
1757 # ==== Fields:
1758 #
1759 # name:: Returns the phrase from [RFC-822] mailbox.
1760 #
1761 # route:: Returns the route from [RFC-822] route-addr.
1762 #
1763 # mailbox:: nil indicates end of [RFC-822] group.
1764 # If non-nil and host is nil, returns [RFC-822] group name.
1765 # Otherwise, returns [RFC-822] local-part
1766 #
1767 # host:: nil indicates [RFC-822] group syntax.
1768 # Otherwise, returns [RFC-822] domain name.
1769 #
1770 Address = Struct.new(:name, :route, :mailbox, :host)
1771
1772 #
1773 # Net::IMAP::ContentDisposition represents Content-Disposition fields.
1774 #
1775 # ==== Fields:
1776 #
1777 # dsp_type:: Returns the disposition type.
1778 #
1779 # param:: Returns a hash that represents parameters of the Content-Disposition
1780 # field.
1781 #
1782 ContentDisposition = Struct.new(:dsp_type, :param)
1783
1784 # Net::IMAP::ThreadMember represents a thread-node returned
1785 # by Net::IMAP#thread
1786 #
1787 # ==== Fields:
1788 #
1789 # seqno:: The sequence number of this message.
1790 #
1791 # children:: an array of Net::IMAP::ThreadMember objects for mail
1792 # items that are children of this in the thread.
1793 #
1794 ThreadMember = Struct.new(:seqno, :children)
1795
1796 # Net::IMAP::BodyTypeBasic represents basic body structures of messages.
1797 #
1798 # ==== Fields:
1799 #
1800 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1801 #
1802 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1803 #
1804 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1805 #
1806 # content_id:: Returns a string giving the content id as defined in [MIME-IMB].
1807 #
1808 # description:: Returns a string giving the content description as defined in
1809 # [MIME-IMB].
1810 #
1811 # encoding:: Returns a string giving the content transfer encoding as defined in
1812 # [MIME-IMB].
1813 #
1814 # size:: Returns a number giving the size of the body in octets.
1815 #
1816 # md5:: Returns a string giving the body MD5 value as defined in [MD5].
1817 #
1818 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1819 # the content disposition.
1820 #
1821 # language:: Returns a string or an array of strings giving the body
1822 # language value as defined in [LANGUAGE-TAGS].
1823 #
1824 # extension:: Returns extension data.
1825 #
1826 # multipart?:: Returns false.
1827 #
1828 class BodyTypeBasic < Struct.new(:media_type, :subtype,
1829 :param, :content_id,
1830 :description, :encoding, :size,
1831 :md5, :disposition, :language,
1832 :extension)
1833 def multipart?
1834 return false
1835 end
1836
1837 # Obsolete: use +subtype+ instead. Calling this will
1838 # generate a warning message to +stderr+, then return
1839 # the value of +subtype+.
1840 def media_subtype
1841 $stderr.printf("warning: media_subtype is obsolete.\n")
1842 $stderr.printf(" use subtype instead.\n")
1843 return subtype
1844 end
1845 end
1846
1847 # Net::IMAP::BodyTypeText represents TEXT body structures of messages.
1848 #
1849 # ==== Fields:
1850 #
1851 # lines:: Returns the size of the body in text lines.
1852 #
1853 # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic.
1854 #
1855 class BodyTypeText < Struct.new(:media_type, :subtype,
1856 :param, :content_id,
1857 :description, :encoding, :size,
1858 :lines,
1859 :md5, :disposition, :language,
1860 :extension)
1861 def multipart?
1862 return false
1863 end
1864
1865 # Obsolete: use +subtype+ instead. Calling this will
1866 # generate a warning message to +stderr+, then return
1867 # the value of +subtype+.
1868 def media_subtype
1869 $stderr.printf("warning: media_subtype is obsolete.\n")
1870 $stderr.printf(" use subtype instead.\n")
1871 return subtype
1872 end
1873 end
1874
1875 # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
1876 #
1877 # ==== Fields:
1878 #
1879 # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure.
1880 #
1881 # body:: Returns an object giving the body structure.
1882 #
1883 # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText.
1884 #
1885 class BodyTypeMessage < Struct.new(:media_type, :subtype,
1886 :param, :content_id,
1887 :description, :encoding, :size,
1888 :envelope, :body, :lines,
1889 :md5, :disposition, :language,
1890 :extension)
1891 def multipart?
1892 return false
1893 end
1894
1895 # Obsolete: use +subtype+ instead. Calling this will
1896 # generate a warning message to +stderr+, then return
1897 # the value of +subtype+.
1898 def media_subtype
1899 $stderr.printf("warning: media_subtype is obsolete.\n")
1900 $stderr.printf(" use subtype instead.\n")
1901 return subtype
1902 end
1903 end
1904
1905 # Net::IMAP::BodyTypeMultipart represents multipart body structures
1906 # of messages.
1907 #
1908 # ==== Fields:
1909 #
1910 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1911 #
1912 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1913 #
1914 # parts:: Returns multiple parts.
1915 #
1916 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1917 #
1918 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1919 # the content disposition.
1920 #
1921 # language:: Returns a string or an array of strings giving the body
1922 # language value as defined in [LANGUAGE-TAGS].
1923 #
1924 # extension:: Returns extension data.
1925 #
1926 # multipart?:: Returns true.
1927 #
1928 class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1929 :parts,
1930 :param, :disposition, :language,
1931 :extension)
1932 def multipart?
1933 return true
1934 end
1935
1936 # Obsolete: use +subtype+ instead. Calling this will
1937 # generate a warning message to +stderr+, then return
1938 # the value of +subtype+.
1939 def media_subtype
1940 $stderr.printf("warning: media_subtype is obsolete.\n")
1941 $stderr.printf(" use subtype instead.\n")
1942 return subtype
1943 end
1944 end
1945
1946 class ResponseParser # :nodoc:
1947 def initialize
1948 @str = nil
1949 @pos = nil
1950 @lex_state = nil
1951 @token = nil
1952 @flag_symbols = {}
1953 end
1954
1955 def parse(str)
1956 @str = str
1957 @pos = 0
1958 @lex_state = EXPR_BEG
1959 @token = nil
1960 return response
1961 end
1962
1963 private
1964
1965 EXPR_BEG = :EXPR_BEG
1966 EXPR_DATA = :EXPR_DATA
1967 EXPR_TEXT = :EXPR_TEXT
1968 EXPR_RTEXT = :EXPR_RTEXT
1969 EXPR_CTEXT = :EXPR_CTEXT
1970
1971 T_SPACE = :SPACE
1972 T_NIL = :NIL
1973 T_NUMBER = :NUMBER
1974 T_ATOM = :ATOM
1975 T_QUOTED = :QUOTED
1976 T_LPAR = :LPAR
1977 T_RPAR = :RPAR
1978 T_BSLASH = :BSLASH
1979 T_STAR = :STAR
1980 T_LBRA = :LBRA
1981 T_RBRA = :RBRA
1982 T_LITERAL = :LITERAL
1983 T_PLUS = :PLUS
1984 T_PERCENT = :PERCENT
1985 T_CRLF = :CRLF
1986 T_EOF = :EOF
1987 T_TEXT = :TEXT
1988
1989 BEG_REGEXP = /\G(?:\
1990 (?# 1: SPACE )( +)|\
1991 (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1992 (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1993 (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
1994 (?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1995 (?# 6: LPAR )(\()|\
1996 (?# 7: RPAR )(\))|\
1997 (?# 8: BSLASH )(\\)|\
1998 (?# 9: STAR )(\*)|\
1999 (?# 10: LBRA )(\[)|\
2000 (?# 11: RBRA )(\])|\
2001 (?# 12: LITERAL )\{(\d+)\}\r\n|\
2002 (?# 13: PLUS )(\+)|\
2003 (?# 14: PERCENT )(%)|\
2004 (?# 15: CRLF )(\r\n)|\
2005 (?# 16: EOF )(\z))/ni
2006
2007 DATA_REGEXP = /\G(?:\
2008 (?# 1: SPACE )( )|\
2009 (?# 2: NIL )(NIL)|\
2010 (?# 3: NUMBER )(\d+)|\
2011 (?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
2012 (?# 5: LITERAL )\{(\d+)\}\r\n|\
2013 (?# 6: LPAR )(\()|\
2014 (?# 7: RPAR )(\)))/ni
2015
2016 TEXT_REGEXP = /\G(?:\
2017 (?# 1: TEXT )([^\x00\r\n]*))/ni
2018
2019 RTEXT_REGEXP = /\G(?:\
2020 (?# 1: LBRA )(\[)|\
2021 (?# 2: TEXT )([^\x00\r\n]*))/ni
2022
2023 CTEXT_REGEXP = /\G(?:\
2024 (?# 1: TEXT )([^\x00\r\n\]]*))/ni
2025
2026 Token = Struct.new(:symbol, :value)
2027
2028 def response
2029 token = lookahead
2030 case token.symbol
2031 when T_PLUS
2032 result = continue_req
2033 when T_STAR
2034 result = response_untagged
2035 else
2036 result = response_tagged
2037 end
2038 match(T_CRLF)
2039 match(T_EOF)
2040 return result
2041 end
2042
2043 def continue_req
2044 match(T_PLUS)
2045 match(T_SPACE)
2046 return ContinuationRequest.new(resp_text, @str)
2047 end
2048
2049 def response_untagged
2050 match(T_STAR)
2051 match(T_SPACE)
2052 token = lookahead
2053 if token.symbol == T_NUMBER
2054 return numeric_response
2055 elsif token.symbol == T_ATOM
2056 case token.value
2057 when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
2058 return response_cond
2059 when /\A(?:FLAGS)\z/ni
2060 return flags_response
2061 when /\A(?:LIST|LSUB)\z/ni
2062 return list_response
2063 when /\A(?:QUOTA)\z/ni
2064 return getquota_response
2065 when /\A(?:QUOTAROOT)\z/ni
2066 return getquotaroot_response
2067 when /\A(?:ACL)\z/ni
2068 return getacl_response
2069 when /\A(?:SEARCH|SORT)\z/ni
2070 return search_response
2071 when /\A(?:THREAD)\z/ni
2072 return thread_response
2073 when /\A(?:STATUS)\z/ni
2074 return status_response
2075 when /\A(?:CAPABILITY)\z/ni
2076 return capability_response
2077 else
2078 return text_response
2079 end
2080 else
2081 parse_error("unexpected token %s", token.symbol)
2082 end
2083 end
2084
2085 def response_tagged
2086 tag = atom
2087 match(T_SPACE)
2088 token = match(T_ATOM)
2089 name = token.value.upcase
2090 match(T_SPACE)
2091 return TaggedResponse.new(tag, name, resp_text, @str)
2092 end
2093
2094 def response_cond
2095 token = match(T_ATOM)
2096 name = token.value.upcase
2097 match(T_SPACE)
2098 return UntaggedResponse.new(name, resp_text, @str)
2099 end
2100
2101 def numeric_response
2102 n = number
2103 match(T_SPACE)
2104 token = match(T_ATOM)
2105 name = token.value.upcase
2106 case name
2107 when "EXISTS", "RECENT", "EXPUNGE"
2108 return UntaggedResponse.new(name, n, @str)
2109 when "FETCH"
2110 shift_token
2111 match(T_SPACE)
2112 data = FetchData.new(n, msg_att)
2113 return UntaggedResponse.new(name, data, @str)
2114 end
2115 end
2116
2117 def msg_att
2118 match(T_LPAR)
2119 attr = {}
2120 while true
2121 token = lookahead
2122 case token.symbol
2123 when T_RPAR
2124 shift_token
2125 break
2126 when T_SPACE
2127 shift_token
2128 token = lookahead
2129 end
2130 case token.value
2131 when /\A(?:ENVELOPE)\z/ni
2132 name, val = envelope_data
2133 when /\A(?:FLAGS)\z/ni
2134 name, val = flags_data
2135 when /\A(?:INTERNALDATE)\z/ni
2136 name, val = internaldate_data
2137 when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
2138 name, val = rfc822_text
2139 when /\A(?:RFC822\.SIZE)\z/ni
2140 name, val = rfc822_size
2141 when /\A(?:BODY(?:STRUCTURE)?)\z/ni
2142 name, val = body_data
2143 when /\A(?:UID)\z/ni
2144 name, val = uid_data
2145 else
2146 parse_error("unknown attribute `%s'", token.value)
2147 end
2148 attr[name] = val
2149 end
2150 return attr
2151 end
2152
2153 def envelope_data
2154 token = match(T_ATOM)
2155 name = token.value.upcase
2156 match(T_SPACE)
2157 return name, envelope
2158 end
2159
2160 def envelope
2161 @lex_state = EXPR_DATA
2162 token = lookahead
2163 if token.symbol == T_NIL
2164 shift_token
2165 result = nil
2166 else
2167 match(T_LPAR)
2168 date = nstring
2169 match(T_SPACE)
2170 subject = nstring
2171 match(T_SPACE)
2172 from = address_list
2173 match(T_SPACE)
2174 sender = address_list
2175 match(T_SPACE)
2176 reply_to = address_list
2177 match(T_SPACE)
2178 to = address_list
2179 match(T_SPACE)
2180 cc = address_list
2181 match(T_SPACE)
2182 bcc = address_list
2183 match(T_SPACE)
2184 in_reply_to = nstring
2185 match(T_SPACE)
2186 message_id = nstring
2187 match(T_RPAR)
2188 result = Envelope.new(date, subject, from, sender, reply_to,
2189 to, cc, bcc, in_reply_to, message_id)
2190 end
2191 @lex_state = EXPR_BEG
2192 return result
2193 end
2194
2195 def flags_data
2196 token = match(T_ATOM)
2197 name = token.value.upcase
2198 match(T_SPACE)
2199 return name, flag_list
2200 end
2201
2202 def internaldate_data
2203 token = match(T_ATOM)
2204 name = token.value.upcase
2205 match(T_SPACE)
2206 token = match(T_QUOTED)
2207 return name, token.value
2208 end
2209
2210 def rfc822_text
2211 token = match(T_ATOM)
2212 name = token.value.upcase
2213 match(T_SPACE)
2214 return name, nstring
2215 end
2216
2217 def rfc822_size
2218 token = match(T_ATOM)
2219 name = token.value.upcase
2220 match(T_SPACE)
2221 return name, number
2222 end
2223
2224 def body_data
2225 token = match(T_ATOM)
2226 name = token.value.upcase
2227 token = lookahead
2228 if token.symbol == T_SPACE
2229 shift_token
2230 return name, body
2231 end
2232 name.concat(section)
2233 token = lookahead
2234 if token.symbol == T_ATOM
2235 name.concat(token.value)
2236 shift_token
2237 end
2238 match(T_SPACE)
2239 data = nstring
2240 return name, data
2241 end
2242
2243 def body
2244 @lex_state = EXPR_DATA
2245 token = lookahead
2246 if token.symbol == T_NIL
2247 shift_token
2248 result = nil
2249 else
2250 match(T_LPAR)
2251 token = lookahead
2252 if token.symbol == T_LPAR
2253 result = body_type_mpart
2254 else
2255 result = body_type_1part
2256 end
2257 match(T_RPAR)
2258 end
2259 @lex_state = EXPR_BEG
2260 return result
2261 end
2262
2263 def body_type_1part
2264 token = lookahead
2265 case token.value
2266 when /\A(?:TEXT)\z/ni
2267 return body_type_text
2268 when /\A(?:MESSAGE)\z/ni
2269 return body_type_msg
2270 else
2271 return body_type_basic
2272 end
2273 end
2274
2275 def body_type_basic
2276 mtype, msubtype = media_type
2277 token = lookahead
2278 if token.symbol == T_RPAR
2279 return BodyTypeBasic.new(mtype, msubtype)
2280 end
2281 match(T_SPACE)
2282 param, content_id, desc, enc, size = body_fields
2283 md5, disposition, language, extension = body_ext_1part
2284 return BodyTypeBasic.new(mtype, msubtype,
2285 param, content_id,
2286 desc, enc, size,
2287 md5, disposition, language, extension)
2288 end
2289
2290 def body_type_text
2291 mtype, msubtype = media_type
2292 match(T_SPACE)
2293 param, content_id, desc, enc, size = body_fields
2294 match(T_SPACE)
2295 lines = number
2296 md5, disposition, language, extension = body_ext_1part
2297 return BodyTypeText.new(mtype, msubtype,
2298 param, content_id,
2299 desc, enc, size,
2300 lines,
2301 md5, disposition, language, extension)
2302 end
2303
2304 def body_type_msg
2305 mtype, msubtype = media_type
2306 match(T_SPACE)
2307 param, content_id, desc, enc, size = body_fields
2308 match(T_SPACE)
2309 env = envelope
2310 match(T_SPACE)
2311 b = body
2312 match(T_SPACE)
2313 lines = number
2314 md5, disposition, language, extension = body_ext_1part
2315 return BodyTypeMessage.new(mtype, msubtype,
2316 param, content_id,
2317 desc, enc, size,
2318 env, b, lines,
2319 md5, disposition, language, extension)
2320 end
2321
2322 def body_type_mpart
2323 parts = []
2324 while true
2325 token = lookahead
2326 if token.symbol == T_SPACE
2327 shift_token
2328 break
2329 end
2330 parts.push(body)
2331 end
2332 mtype = "MULTIPART"
2333 msubtype = case_insensitive_string
2334 param, disposition, language, extension = body_ext_mpart
2335 return BodyTypeMultipart.new(mtype, msubtype, parts,
2336 param, disposition, language,
2337 extension)
2338 end
2339
2340 def media_type
2341 mtype = case_insensitive_string
2342 match(T_SPACE)
2343 msubtype = case_insensitive_string
2344 return mtype, msubtype
2345 end
2346
2347 def body_fields
2348 param = body_fld_param
2349 match(T_SPACE)
2350 content_id = nstring
2351 match(T_SPACE)
2352 desc = nstring
2353 match(T_SPACE)
2354 enc = case_insensitive_string
2355 match(T_SPACE)
2356 size = number
2357 return param, content_id, desc, enc, size
2358 end
2359
2360 def body_fld_param
2361 token = lookahead
2362 if token.symbol == T_NIL
2363 shift_token
2364 return nil
2365 end
2366 match(T_LPAR)
2367 param = {}
2368 while true
2369 token = lookahead
2370 case token.symbol
2371 when T_RPAR
2372 shift_token
2373 break
2374 when T_SPACE
2375 shift_token
2376 end
2377 name = case_insensitive_string
2378 match(T_SPACE)
2379 val = string
2380 param[name] = val
2381 end
2382 return param
2383 end
2384
2385 def body_ext_1part
2386 token = lookahead
2387 if token.symbol == T_SPACE
2388 shift_token
2389 else
2390 return nil
2391 end
2392 md5 = nstring
2393
2394 token = lookahead
2395 if token.symbol == T_SPACE
2396 shift_token
2397 else
2398 return md5
2399 end
2400 disposition = body_fld_dsp
2401
2402 token = lookahead
2403 if token.symbol == T_SPACE
2404 shift_token
2405 else
2406 return md5, disposition
2407 end
2408 language = body_fld_lang
2409
2410 token = lookahead
2411 if token.symbol == T_SPACE
2412 shift_token
2413 else
2414 return md5, disposition, language
2415 end
2416
2417 extension = body_extensions
2418 return md5, disposition, language, extension
2419 end
2420
2421 def body_ext_mpart
2422 token = lookahead
2423 if token.symbol == T_SPACE
2424 shift_token
2425 else
2426 return nil
2427 end
2428 param = body_fld_param
2429
2430 token = lookahead
2431 if token.symbol == T_SPACE
2432 shift_token
2433 else
2434 return param
2435 end
2436 disposition = body_fld_dsp
2437 match(T_SPACE)
2438 language = body_fld_lang
2439
2440 token = lookahead
2441 if token.symbol == T_SPACE
2442 shift_token
2443 else
2444 return param, disposition, language
2445 end
2446
2447 extension = body_extensions
2448 return param, disposition, language, extension
2449 end
2450
2451 def body_fld_dsp
2452 token = lookahead
2453 if token.symbol == T_NIL
2454 shift_token
2455 return nil
2456 end
2457 match(T_LPAR)
2458 dsp_type = case_insensitive_string
2459 match(T_SPACE)
2460 param = body_fld_param
2461 match(T_RPAR)
2462 return ContentDisposition.new(dsp_type, param)
2463 end
2464
2465 def body_fld_lang
2466 token = lookahead
2467 if token.symbol == T_LPAR
2468 shift_token
2469 result = []
2470 while true
2471 token = lookahead
2472 case token.symbol
2473 when T_RPAR
2474 shift_token
2475 return result
2476 when T_SPACE
2477 shift_token
2478 end
2479 result.push(case_insensitive_string)
2480 end
2481 else
2482 lang = nstring
2483 if lang
2484 return lang.upcase
2485 else
2486 return lang
2487 end
2488 end
2489 end
2490
2491 def body_extensions
2492 result = []
2493 while true
2494 token = lookahead
2495 case token.symbol
2496 when T_RPAR
2497 return result
2498 when T_SPACE
2499 shift_token
2500 end
2501 result.push(body_extension)
2502 end
2503 end
2504
2505 def body_extension
2506 token = lookahead
2507 case token.symbol
2508 when T_LPAR
2509 shift_token
2510 result = body_extensions
2511 match(T_RPAR)
2512 return result
2513 when T_NUMBER
2514 return number
2515 else
2516 return nstring
2517 end
2518 end
2519
2520 def section
2521 str = ""
2522 token = match(T_LBRA)
2523 str.concat(token.value)
2524 token = match(T_ATOM, T_NUMBER, T_RBRA)
2525 if token.symbol == T_RBRA
2526 str.concat(token.value)
2527 return str
2528 end
2529 str.concat(token.value)
2530 token = lookahead
2531 if token.symbol == T_SPACE
2532 shift_token
2533 str.concat(token.value)
2534 token = match(T_LPAR)
2535 str.concat(token.value)
2536 while true
2537 token = lookahead
2538 case token.symbol
2539 when T_RPAR
2540 str.concat(token.value)
2541 shift_token
2542 break
2543 when T_SPACE
2544 shift_token
2545 str.concat(token.value)
2546 end
2547 str.concat(format_string(astring))
2548 end
2549 end
2550 token = match(T_RBRA)
2551 str.concat(token.value)
2552 return str
2553 end
2554
2555 def format_string(str)
2556 case str
2557 when ""
2558 return '""'
2559 when /[\x80-\xff\r\n]/n
2560 # literal
2561 return "{" + str.length.to_s + "}" + CRLF + str
2562 when /[(){ \x00-\x1f\x7f%*"\\]/n
2563 # quoted string
2564 return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
2565 else
2566 # atom
2567 return str
2568 end
2569 end
2570
2571 def uid_data
2572 token = match(T_ATOM)
2573 name = token.value.upcase
2574 match(T_SPACE)
2575 return name, number
2576 end
2577
2578 def text_response
2579 token = match(T_ATOM)
2580 name = token.value.upcase
2581 match(T_SPACE)
2582 @lex_state = EXPR_TEXT
2583 token = match(T_TEXT)
2584 @lex_state = EXPR_BEG
2585 return UntaggedResponse.new(name, token.value)
2586 end
2587
2588 def flags_response
2589 token = match(T_ATOM)
2590 name = token.value.upcase
2591 match(T_SPACE)
2592 return UntaggedResponse.new(name, flag_list, @str)
2593 end
2594
2595 def list_response
2596 token = match(T_ATOM)
2597 name = token.value.upcase
2598 match(T_SPACE)
2599 return UntaggedResponse.new(name, mailbox_list, @str)
2600 end
2601
2602 def mailbox_list
2603 attr = flag_list
2604 match(T_SPACE)
2605 token = match(T_QUOTED, T_NIL)
2606 if token.symbol == T_NIL
2607 delim = nil
2608 else
2609 delim = token.value
2610 end
2611 match(T_SPACE)
2612 name = astring
2613 return MailboxList.new(attr, delim, name)
2614 end
2615
2616 def getquota_response
2617 # If quota never established, get back
2618 # `NO Quota root does not exist'.
2619 # If quota removed, get `()' after the
2620 # folder spec with no mention of `STORAGE'.
2621 token = match(T_ATOM)
2622 name = token.value.upcase
2623 match(T_SPACE)
2624 mailbox = astring
2625 match(T_SPACE)
2626 match(T_LPAR)
2627 token = lookahead
2628 case token.symbol
2629 when T_RPAR
2630 shift_token
2631 data = MailboxQuota.new(mailbox, nil, nil)
2632 return UntaggedResponse.new(name, data, @str)
2633 when T_ATOM
2634 shift_token
2635 match(T_SPACE)
2636 token = match(T_NUMBER)
2637 usage = token.value
2638 match(T_SPACE)
2639 token = match(T_NUMBER)
2640 quota = token.value
2641 match(T_RPAR)
2642 data = MailboxQuota.new(mailbox, usage, quota)
2643 return UntaggedResponse.new(name, data, @str)
2644 else
2645 parse_error("unexpected token %s", token.symbol)
2646 end
2647 end
2648
2649 def getquotaroot_response
2650 # Similar to getquota, but only admin can use getquota.
2651 token = match(T_ATOM)
2652 name = token.value.upcase
2653 match(T_SPACE)
2654 mailbox = astring
2655 quotaroots = []
2656 while true
2657 token = lookahead
2658 break unless token.symbol == T_SPACE
2659 shift_token
2660 quotaroots.push(astring)
2661 end
2662 data = MailboxQuotaRoot.new(mailbox, quotaroots)
2663 return UntaggedResponse.new(name, data, @str)
2664 end
2665
2666 def getacl_response
2667 token = match(T_ATOM)
2668 name = token.value.upcase
2669 match(T_SPACE)
2670 mailbox = astring
2671 data = []
2672 token = lookahead
2673 if token.symbol == T_SPACE
2674 shift_token
2675 while true
2676 token = lookahead
2677 case token.symbol
2678 when T_CRLF
2679 break
2680 when T_SPACE
2681 shift_token
2682 end
2683 user = astring
2684 match(T_SPACE)
2685 rights = astring
2686 ##XXX data.push([user, rights])
2687 data.push(MailboxACLItem.new(user, rights))
2688 end
2689 end
2690 return UntaggedResponse.new(name, data, @str)
2691 end
2692
2693 def search_response
2694 token = match(T_ATOM)
2695 name = token.value.upcase
2696 token = lookahead
2697 if token.symbol == T_SPACE
2698 shift_token
2699 data = []
2700 while true
2701 token = lookahead
2702 case token.symbol
2703 when T_CRLF
2704 break
2705 when T_SPACE
2706 shift_token
2707 end
2708 data.push(number)
2709 end
2710 else
2711 data = []
2712 end
2713 return UntaggedResponse.new(name, data, @str)
2714 end
2715
2716 def thread_response
2717 token = match(T_ATOM)
2718 name = token.value.upcase
2719 token = lookahead
2720
2721 if token.symbol == T_SPACE
2722 threads = []
2723
2724 while true
2725 shift_token
2726 token = lookahead
2727
2728 case token.symbol
2729 when T_LPAR
2730 threads << thread_branch(token)
2731 when T_CRLF
2732 break
2733 end
2734 end
2735 else
2736 # no member
2737 threads = []
2738 end
2739
2740 return UntaggedResponse.new(name, threads, @str)
2741 end
2742
2743 def thread_branch(token)
2744 rootmember = nil
2745 lastmember = nil
2746
2747 while true
2748 shift_token # ignore first T_LPAR
2749 token = lookahead
2750
2751 case token.symbol
2752 when T_NUMBER
2753 # new member
2754 newmember = ThreadMember.new(number, [])
2755 if rootmember.nil?
2756 rootmember = newmember
2757 else
2758 lastmember.children << newmember
2759 end
2760 lastmember = newmember
2761 when T_SPACE
2762 # do nothing
2763 when T_LPAR
2764 if rootmember.nil?
2765 # dummy member
2766 lastmember = rootmember = ThreadMember.new(nil, [])
2767 end
2768
2769 lastmember.children << thread_branch(token)
2770 when T_RPAR
2771 break
2772 end
2773 end
2774
2775 return rootmember
2776 end
2777
2778 def status_response
2779 token = match(T_ATOM)
2780 name = token.value.upcase
2781 match(T_SPACE)
2782 mailbox = astring
2783 match(T_SPACE)
2784 match(T_LPAR)
2785 attr = {}
2786 while true
2787 token = lookahead
2788 case token.symbol
2789 when T_RPAR
2790 shift_token
2791 break
2792 when T_SPACE
2793 shift_token
2794 end
2795 token = match(T_ATOM)
2796 key = token.value.upcase
2797 match(T_SPACE)
2798 val = number
2799 attr[key] = val
2800 end
2801 data = StatusData.new(mailbox, attr)
2802 return UntaggedResponse.new(name, data, @str)
2803 end
2804
2805 def capability_response
2806 token = match(T_ATOM)
2807 name = token.value.upcase
2808 match(T_SPACE)
2809 data = []
2810 while true
2811 token = lookahead
2812 case token.symbol
2813 when T_CRLF
2814 break
2815 when T_SPACE
2816 shift_token
2817 end
2818 data.push(atom.upcase)
2819 end
2820 return UntaggedResponse.new(name, data, @str)
2821 end
2822
2823 def resp_text
2824 @lex_state = EXPR_RTEXT
2825 token = lookahead
2826 if token.symbol == T_LBRA
2827 code = resp_text_code
2828 else
2829 code = nil
2830 end
2831 token = match(T_TEXT)
2832 @lex_state = EXPR_BEG
2833 return ResponseText.new(code, token.value)
2834 end
2835
2836 def resp_text_code
2837 @lex_state = EXPR_BEG
2838 match(T_LBRA)
2839 token = match(T_ATOM)
2840 name = token.value.upcase
2841 case name
2842 when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
2843 result = ResponseCode.new(name, nil)
2844 when /\A(?:PERMANENTFLAGS)\z/n
2845 match(T_SPACE)
2846 result = ResponseCode.new(name, flag_list)
2847 when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
2848 match(T_SPACE)
2849 result = ResponseCode.new(name, number)
2850 else
2851 token = lookahead
2852 if token.symbol == T_SPACE
2853 shift_token
2854 @lex_state = EXPR_CTEXT
2855 token = match(T_TEXT)
2856 @lex_state = EXPR_BEG
2857 result = ResponseCode.new(name, token.value)
2858 else
2859 result = ResponseCode.new(name, nil)
2860 end
2861 end
2862 match(T_RBRA)
2863 @lex_state = EXPR_RTEXT
2864 return result
2865 end
2866
2867 def address_list
2868 token = lookahead
2869 if token.symbol == T_NIL
2870 shift_token
2871 return nil
2872 else
2873 result = []
2874 match(T_LPAR)
2875 while true
2876 token = lookahead
2877 case token.symbol
2878 when T_RPAR
2879 shift_token
2880 break
2881 when T_SPACE
2882 shift_token
2883 end
2884 result.push(address)
2885 end
2886 return result
2887 end
2888 end
2889
2890 ADDRESS_REGEXP = /\G\
2891 (?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2892 (?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2893 (?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2894 (?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
2895 \)/ni
2896
2897 def address
2898 match(T_LPAR)
2899 if @str.index(ADDRESS_REGEXP, @pos)
2900 # address does not include literal.
2901 @pos = $~.end(0)
2902 name = $1
2903 route = $2
2904 mailbox = $3
2905 host = $4
2906 for s in [name, route, mailbox, host]
2907 if s
2908 s.gsub!(/\\(["\\])/n, "\\1")
2909 end
2910 end
2911 else
2912 name = nstring
2913 match(T_SPACE)
2914 route = nstring
2915 match(T_SPACE)
2916 mailbox = nstring
2917 match(T_SPACE)
2918 host = nstring
2919 match(T_RPAR)
2920 end
2921 return Address.new(name, route, mailbox, host)
2922 end
2923
2924 # def flag_list
2925 # result = []
2926 # match(T_LPAR)
2927 # while true
2928 # token = lookahead
2929 # case token.symbol
2930 # when T_RPAR
2931 # shift_token
2932 # break
2933 # when T_SPACE
2934 # shift_token
2935 # end
2936 # result.push(flag)
2937 # end
2938 # return result
2939 # end
2940
2941 # def flag
2942 # token = lookahead
2943 # if token.symbol == T_BSLASH
2944 # shift_token
2945 # token = lookahead
2946 # if token.symbol == T_STAR
2947 # shift_token
2948 # return token.value.intern
2949 # else
2950 # return atom.intern
2951 # end
2952 # else
2953 # return atom
2954 # end
2955 # end
2956
2957 FLAG_REGEXP = /\
2958 (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
2959 (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
2960
2961 def flag_list
2962 if @str.index(/\(([^)]*)\)/ni, @pos)
2963 @pos = $~.end(0)
2964 return $1.scan(FLAG_REGEXP).collect { |flag, atom|
2965 if atom
2966 atom
2967 else
2968 symbol = flag.capitalize.untaint.intern
2969 @flag_symbols[symbol] = true
2970 if @flag_symbols.length > IMAP.max_flag_count
2971 raise FlagCountError, "number of flag symbols exceeded"
2972 end
2973 symbol
2974 end
2975 }
2976 else
2977 parse_error("invalid flag list")
2978 end
2979 end
2980
2981 def nstring
2982 token = lookahead
2983 if token.symbol == T_NIL
2984 shift_token
2985 return nil
2986 else
2987 return string
2988 end
2989 end
2990
2991 def astring
2992 token = lookahead
2993 if string_token?(token)
2994 return string
2995 else
2996 return atom
2997 end
2998 end
2999
3000 def string
3001 token = lookahead
3002 if token.symbol == T_NIL
3003 shift_token
3004 return nil
3005 end
3006 token = match(T_QUOTED, T_LITERAL)
3007 return token.value
3008 end
3009
3010 STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
3011
3012 def string_token?(token)
3013 return STRING_TOKENS.include?(token.symbol)
3014 end
3015
3016 def case_insensitive_string
3017 token = lookahead
3018 if token.symbol == T_NIL
3019 shift_token
3020 return nil
3021 end
3022 token = match(T_QUOTED, T_LITERAL)
3023 return token.value.upcase
3024 end
3025
3026 def atom
3027 result = ""
3028 while true
3029 token = lookahead
3030 if atom_token?(token)
3031 result.concat(token.value)
3032 shift_token
3033 else
3034 if result.empty?
3035 parse_error("unexpected token %s", token.symbol)
3036 else
3037 return result
3038 end
3039 end
3040 end
3041 end
3042
3043 ATOM_TOKENS = [
3044 T_ATOM,
3045 T_NUMBER,
3046 T_NIL,
3047 T_LBRA,
3048 T_RBRA,
3049 T_PLUS
3050 ]
3051
3052 def atom_token?(token)
3053 return ATOM_TOKENS.include?(token.symbol)
3054 end
3055
3056 def number
3057 token = lookahead
3058 if token.symbol == T_NIL
3059 shift_token
3060 return nil
3061 end
3062 token = match(T_NUMBER)
3063 return token.value.to_i
3064 end
3065
3066 def nil_atom
3067 match(T_NIL)
3068 return nil
3069 end
3070
3071 def match(*args)
3072 token = lookahead
3073 unless args.include?(token.symbol)
3074 parse_error('unexpected token %s (expected %s)',
3075 token.symbol.id2name,
3076 args.collect {|i| i.id2name}.join(" or "))
3077 end
3078 shift_token
3079 return token
3080 end
3081
3082 def lookahead
3083 unless @token
3084 @token = next_token
3085 end
3086 return @token
3087 end
3088
3089 def shift_token
3090 @token = nil
3091 end
3092
3093 def next_token
3094 case @lex_state
3095 when EXPR_BEG
3096 if @str.index(BEG_REGEXP, @pos)
3097 @pos = $~.end(0)
3098 if $1
3099 return Token.new(T_SPACE, $+)
3100 elsif $2
3101 return Token.new(T_NIL, $+)
3102 elsif $3
3103 return Token.new(T_NUMBER, $+)
3104 elsif $4
3105 return Token.new(T_ATOM, $+)
3106 elsif $5
3107 return Token.new(T_QUOTED,
3108 $+.gsub(/\\(["\\])/n, "\\1"))
3109 elsif $6
3110 return Token.new(T_LPAR, $+)
3111 elsif $7
3112 return Token.new(T_RPAR, $+)
3113 elsif $8
3114 return Token.new(T_BSLASH, $+)
3115 elsif $9
3116 return Token.new(T_STAR, $+)
3117 elsif $10
3118 return Token.new(T_LBRA, $+)
3119 elsif $11
3120 return Token.new(T_RBRA, $+)
3121 elsif $12
3122 len = $+.to_i
3123 val = @str[@pos, len]
3124 @pos += len
3125 return Token.new(T_LITERAL, val)
3126 elsif $13
3127 return Token.new(T_PLUS, $+)
3128 elsif $14
3129 return Token.new(T_PERCENT, $+)
3130 elsif $15
3131 return Token.new(T_CRLF, $+)
3132 elsif $16
3133 return Token.new(T_EOF, $+)
3134 else
3135 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
3136 end
3137 else
3138 @str.index(/\S*/n, @pos)
3139 parse_error("unknown token - %s", $&.dump)
3140 end
3141 when EXPR_DATA
3142 if @str.index(DATA_REGEXP, @pos)
3143 @pos = $~.end(0)
3144 if $1
3145 return Token.new(T_SPACE, $+)
3146 elsif $2
3147 return Token.new(T_NIL, $+)
3148 elsif $3
3149 return Token.new(T_NUMBER, $+)
3150 elsif $4
3151 return Token.new(T_QUOTED,
3152 $+.gsub(/\\(["\\])/n, "\\1"))
3153 elsif $5
3154 len = $+.to_i
3155 val = @str[@pos, len]
3156 @pos += len
3157 return Token.new(T_LITERAL, val)
3158 elsif $6
3159 return Token.new(T_LPAR, $+)
3160 elsif $7
3161 return Token.new(T_RPAR, $+)
3162 else
3163 parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
3164 end
3165 else
3166 @str.index(/\S*/n, @pos)
3167 parse_error("unknown token - %s", $&.dump)
3168 end
3169 when EXPR_TEXT
3170 if @str.index(TEXT_REGEXP, @pos)
3171 @pos = $~.end(0)
3172 if $1
3173 return Token.new(T_TEXT, $+)
3174 else
3175 parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
3176 end
3177 else
3178 @str.index(/\S*/n, @pos)
3179 parse_error("unknown token - %s", $&.dump)
3180 end
3181 when EXPR_RTEXT
3182 if @str.index(RTEXT_REGEXP, @pos)
3183 @pos = $~.end(0)
3184 if $1
3185 return Token.new(T_LBRA, $+)
3186 elsif $2
3187 return Token.new(T_TEXT, $+)
3188 else
3189 parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
3190 end
3191 else
3192 @str.index(/\S*/n, @pos)
3193 parse_error("unknown token - %s", $&.dump)
3194 end
3195 when EXPR_CTEXT
3196 if @str.index(CTEXT_REGEXP, @pos)
3197 @pos = $~.end(0)
3198 if $1
3199 return Token.new(T_TEXT, $+)
3200 else
3201 parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
3202 end
3203 else
3204 @str.index(/\S*/n, @pos) #/
3205 parse_error("unknown token - %s", $&.dump)
3206 end
3207 else
3208 parse_error("invalid @lex_state - %s", @lex_state.inspect)
3209 end
3210 end
3211
3212 def parse_error(fmt, *args)
3213 if IMAP.debug
3214 $stderr.printf("@str: %s\n", @str.dump)
3215 $stderr.printf("@pos: %d\n", @pos)
3216 $stderr.printf("@lex_state: %s\n", @lex_state)
3217 if @token
3218 $stderr.printf("@token.symbol: %s\n", @token.symbol)
3219 $stderr.printf("@token.value: %s\n", @token.value.inspect)
3220 end
3221 end
3222 raise ResponseParseError, format(fmt, *args)
3223 end
3224 end
3225
3226 # Authenticator for the "LOGIN" authentication type. See
3227 # #authenticate().
3228 class LoginAuthenticator
3229 def process(data)
3230 case @state
3231 when STATE_USER
3232 @state = STATE_PASSWORD
3233 return @user
3234 when STATE_PASSWORD
3235 return @password
3236 end
3237 end
3238
3239 private
3240
3241 STATE_USER = :USER
3242 STATE_PASSWORD = :PASSWORD
3243
3244 def initialize(user, password)
3245 @user = user
3246 @password = password
3247 @state = STATE_USER
3248 end
3249 end
3250 add_authenticator "LOGIN", LoginAuthenticator
3251
3252 # Authenticator for the "PLAIN" authentication type. See
3253 # #authenticate().
3254 class PlainAuthenticator
3255 def process(data)
3256 return "\0#{@user}\0#{@password}"
3257 end
3258
3259 private
3260
3261 def initialize(user, password)
3262 @user = user
3263 @password = password
3264 end
3265 end
3266 add_authenticator "PLAIN", PlainAuthenticator
3267
3268 # Authenticator for the "CRAM-MD5" authentication type. See
3269 # #authenticate().
3270 class CramMD5Authenticator
3271 def process(challenge)
3272 digest = hmac_md5(challenge, @password)
3273 return @user + " " + digest
3274 end
3275
3276 private
3277
3278 def initialize(user, password)
3279 @user = user
3280 @password = password
3281 end
3282
3283 def hmac_md5(text, key)
3284 if key.length > 64
3285 key = Digest::MD5.digest(key)
3286 end
3287
3288 k_ipad = key + "\0" * (64 - key.length)
3289 k_opad = key + "\0" * (64 - key.length)
3290 for i in 0..63
3291 k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
3292 k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
3293 end
3294
3295 digest = Digest::MD5.digest(k_ipad + text)
3296
3297 return Digest::MD5.hexdigest(k_opad + digest)
3298 end
3299 end
3300 add_authenticator "CRAM-MD5", CramMD5Authenticator
3301
3302 # Authenticator for the "DIGEST-MD5" authentication type. See
3303 # #authenticate().
3304 class DigestMD5Authenticator
3305 def process(challenge)
3306 case @stage
3307 when STAGE_ONE
3308 @stage = STAGE_TWO
3309 sparams = {}
3310 c = StringScanner.new(challenge)
3311 while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
3312 k, v = c[1], c[2]
3313 if v =~ /^"(.*)"$/
3314 v = $1
3315 if v =~ /,/
3316 v = v.split(',')
3317 end
3318 end
3319 sparams[k] = v
3320 end
3321
3322 raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
3323 raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
3324
3325 response = {
3326 :nonce => sparams['nonce'],
3327 :username => @user,
3328 :realm => sparams['realm'],
3329 :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
3330 :'digest-uri' => 'imap/' + sparams['realm'],
3331 :qop => 'auth',
3332 :maxbuf => 65535,
3333 :nc => "%08d" % nc(sparams['nonce']),
3334 :charset => sparams['charset'],
3335 }
3336
3337 response[:authzid] = @authname unless @authname.nil?
3338
3339 # now, the real thing
3340 a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
3341
3342 a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
3343 a1 << ':' + response[:authzid] unless response[:authzid].nil?
3344
3345 a2 = "AUTHENTICATE:" + response[:'digest-uri']
3346 a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
3347
3348 response[:response] = Digest::MD5.hexdigest(
3349 [
3350 Digest::MD5.hexdigest(a1),
3351 response.values_at(:nonce, :nc, :cnonce, :qop),
3352 Digest::MD5.hexdigest(a2)
3353 ].join(':')
3354 )
3355
3356 return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
3357 when STAGE_TWO
3358 @stage = nil
3359 # if at the second stage, return an empty string
3360 if challenge =~ /rspauth=/
3361 return ''
3362 else
3363 raise ResponseParseError, challenge
3364 end
3365 else
3366 raise ResponseParseError, challenge
3367 end
3368 end
3369
3370 def initialize(user, password, authname = nil)
3371 @user, @password, @authname = user, password, authname
3372 @nc, @stage = {}, STAGE_ONE
3373 end
3374
3375 private
3376
3377 STAGE_ONE = :stage_one
3378 STAGE_TWO = :stage_two
3379
3380 def nc(nonce)
3381 if @nc.has_key? nonce
3382 @nc[nonce] = @nc[nonce] + 1
3383 else
3384 @nc[nonce] = 1
3385 end
3386 return @nc[nonce]
3387 end
3388
3389 # some responses need quoting
3390 def qdval(k, v)
3391 return if k.nil? or v.nil?
3392 if %w"username authzid realm nonce cnonce digest-uri qop".include? k
3393 v.gsub!(/([\\"])/, "\\\1")
3394 return '%s="%s"' % [k, v]
3395 else
3396 return '%s=%s' % [k, v]
3397 end
3398 end
3399 end
3400 add_authenticator "DIGEST-MD5", DigestMD5Authenticator
3401
3402 # Superclass of IMAP errors.
3403 class Error < StandardError
3404 end
3405
3406 # Error raised when data is in the incorrect format.
3407 class DataFormatError < Error
3408 end
3409
3410 # Error raised when a response from the server is non-parseable.
3411 class ResponseParseError < Error
3412 end
3413
3414 # Superclass of all errors used to encapsulate "fail" responses
3415 # from the server.
3416 class ResponseError < Error
3417
3418 # The response that caused this error
3419 attr_accessor :response
3420
3421 def initialize(response)
3422 @response = response
3423
3424 super @response.data.text
3425 end
3426
3427 end
3428
3429 # Error raised upon a "NO" response from the server, indicating
3430 # that the client command could not be completed successfully.
3431 class NoResponseError < ResponseError
3432 end
3433
3434 # Error raised upon a "BAD" response from the server, indicating
3435 # that the client command violated the IMAP protocol, or an internal
3436 # server failure has occurred.
3437 class BadResponseError < ResponseError
3438 end
3439
3440 # Error raised upon a "BYE" response from the server, indicating
3441 # that the client is not being allowed to login, or has been timed
3442 # out due to inactivity.
3443 class ByeResponseError < ResponseError
3444 end
3445
3446 # Error raised when too many flags are interned to symbols.
3447 class FlagCountError < Error
3448 end
3449 end
3450 end
3451
3452 if __FILE__ == $0
3453 # :enddoc:
3454 require "getoptlong"
3455
3456 $stdout.sync = true
3457 $port = nil
3458 $user = ENV["USER"] || ENV["LOGNAME"]
3459 $auth = "login"
3460 $ssl = false
3461
3462 def usage
3463 $stderr.print <<EOF
3464 usage: #{$0} [options] <host>
3465
3466 --help print this message
3467 --port=PORT specifies port
3468 --user=USER specifies user
3469 --auth=AUTH specifies auth type
3470 --ssl use ssl
3471 EOF
3472 end
3473
3474 def get_password
3475 print "password: "
3476 system("stty", "-echo")
3477 begin
3478 return gets.chop
3479 ensure
3480 system("stty", "echo")
3481 print "\n"
3482 end
3483 end
3484
3485 def get_command
3486 printf("%s@%s> ", $user, $host)
3487 if line = gets
3488 return line.strip.split(/\s+/)
3489 else
3490 return nil
3491 end
3492 end
3493
3494 parser = GetoptLong.new
3495 parser.set_options(['--debug', GetoptLong::NO_ARGUMENT],
3496 ['--help', GetoptLong::NO_ARGUMENT],
3497 ['--port', GetoptLong::REQUIRED_ARGUMENT],
3498 ['--user', GetoptLong::REQUIRED_ARGUMENT],
3499 ['--auth', GetoptLong::REQUIRED_ARGUMENT],
3500 ['--ssl', GetoptLong::NO_ARGUMENT])
3501 begin
3502 parser.each_option do |name, arg|
3503 case name
3504 when "--port"
3505 $port = arg
3506 when "--user"
3507 $user = arg
3508 when "--auth"
3509 $auth = arg
3510 when "--ssl"
3511 $ssl = true
3512 when "--debug"
3513 Net::IMAP.debug = true
3514 when "--help"
3515 usage
3516 exit(1)
3517 end
3518 end
3519 rescue
3520 usage
3521 exit(1)
3522 end
3523
3524 $host = ARGV.shift
3525 unless $host
3526 usage
3527 exit(1)
3528 end
3529
3530 imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl)
3531 begin
3532 password = get_password
3533 imap.authenticate($auth, $user, password)
3534 while true
3535 cmd, *args = get_command
3536 break unless cmd
3537 begin
3538 case cmd
3539 when "list"
3540 for mbox in imap.list("", args[0] || "*")
3541 if mbox.attr.include?(Net::IMAP::NOSELECT)
3542 prefix = "!"
3543 elsif mbox.attr.include?(Net::IMAP::MARKED)
3544 prefix = "*"
3545 else
3546 prefix = " "
3547 end
3548 print prefix, mbox.name, "\n"
3549 end
3550 when "select"
3551 imap.select(args[0] || "inbox")
3552 print "ok\n"
3553 when "close"
3554 imap.close
3555 print "ok\n"
3556 when "summary"
3557 unless messages = imap.responses["EXISTS"][-1]
3558 puts "not selected"
3559 next
3560 end
3561 if messages > 0
3562 for data in imap.fetch(1..-1, ["ENVELOPE"])
3563 print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
3564 end
3565 else
3566 puts "no message"
3567 end
3568 when "fetch"
3569 if args[0]
3570 data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
3571 puts data.attr["RFC822.HEADER"]
3572 puts data.attr["RFC822.TEXT"]
3573 else
3574 puts "missing argument"
3575 end
3576 when "logout", "exit", "quit"
3577 break
3578 when "help", "?"
3579 print <<EOF
3580 list [pattern] list mailboxes
3581 select [mailbox] select mailbox
3582 close close mailbox
3583 summary display summary
3584 fetch [msgno] display message
3585 logout logout
3586 help, ? display help message
3587 EOF
3588 else
3589 print "unknown command: ", cmd, "\n"
3590 end
3591 rescue Net::IMAP::Error
3592 puts $!
3593 end
3594 end
3595 ensure
3596 imap.logout
3597 imap.disconnect
3598 end
3599 end
3600
0 Date: Mon, 12 Aug 2013 16:25:20 +0200
1 From: Antonio Terceiro <terceiro@debian.org>
2 To: terceiro@debian.org
3 Subject: UTF-8 data: =?iso-8859-1?B?4ent8/o=?=
4 Message-ID: <regular-message-id@debian.org>
5 MIME-Version: 1.0
6 Content-Type: text/plain; charset=us-ascii
7 Content-Disposition: inline
8 User-Agent: Mutt/1.5.21 (2010-09-15)
9
10 This is a sample email
0 Date: Mon, 12 Aug 2013 16:52:17 +0200
1 From: Antonio Terceiro <terceiro@debian.org>
2 To: terceiro@debian.org
3 Subject: an unread message
4 Message-ID: <unread-message-id@debian.org>
5 MIME-Version: 1.0
6 Content-Type: text/plain; charset=us-ascii
7 Content-Disposition: inline
8 User-Agent: Mutt/1.5.21 (2010-09-15)
9
10 This message was not read yet
0 Date: Mon, 12 Aug 2013 17:07:02 +0200
1 From: Antonio Terceiro <terceiro@debian.org>
2 To: terceiro@debian.org
3 Subject: a flagged message
4 Message-ID: <flagged-message-id@debian.org>
5 MIME-Version: 1.0
6 Content-Type: text/plain; charset=us-ascii
7 Content-Disposition: inline
8 User-Agent: Mutt/1.5.21 (2010-09-15)
9
10 This message is flagged.
0 Date: Mon, 12 Aug 2013 17:08:19 +0200
1 From: Antonio Terceiro <terceiro@debian.org>
2 To: terceiro@debian.org
3 Subject: a new message
4 Message-ID: <new-message-id@debian.org>
5 MIME-Version: 1.0
6 Content-Type: text/plain; charset=us-ascii
7 Content-Disposition: inline
8 User-Agent: Mutt/1.5.21 (2010-09-15)
9
10 This message is new
3333 url: http://something2
3434 target: imaps://login:pasword@ezaezae/Feeds/B
3535 EOF
36 CONFPARTS = <<EOF
37 parts: text
38 include-images: false
39 feeds:
40 - name: feed1
41 url: http://something
42 target: imap://login:pasword@ezaezae/Feeds/A
43 - name: feed2
44 url: http://something2
45 target: imap://login:pasword@ezaezae/Feeds/B
46 EOF
3647
3748 class ConfigTest < Test::Unit::TestCase
3849 def test_cache
6071 assert_equal('http://something', conf.feeds[0].url)
6172 assert_equal('http://something2', conf.feeds[1].url)
6273 end
74
75 def test_parts
76 sio = StringIO::new CONFPARTS
77 conf = F2IConfig::new(sio)
78 assert conf.parts.include?('text')
79 assert ! conf.parts.include?('html')
80 end
6381 end
+0
-14
test/tc_mail.rb less more
0 #!/usr/bin/ruby -w
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
4 require 'test/unit'
5 require 'rmail'
6
7 class MailTest < Test::Unit::TestCase
8 def test_require_rmail
9 # let's just test Rubymail is loaded
10 m = RMail::Message::new
11 assert_equal(m.class, RMail::Message)
12 end
13 end
0 require 'test/unit'
1 require 'fileutils'
2 require 'tmpdir'
3 require 'mocha/setup'
4
5 require 'feed2imap/maildir'
6
7 class TestMaildir < Test::Unit::TestCase
8
9 def setup
10 @tmpdirs = []
11 end
12
13 def tear_down
14 @tmpdirs.each do |dir|
15 FileUtils.rm_rf(dir)
16 end
17 end
18
19 def test_cleanup
20 folder = create_maildir
21 msgs = message_count(folder)
22
23 maildir_account.cleanup(folder)
24
25 assert_equal msgs - 1, message_count(folder)
26 end
27
28 def test_putmail
29 folder = create_maildir
30 msgs = message_count(folder)
31
32 mail = RMail::Message.new
33 mail.header['Subject'] = 'a message I just created'
34 mail.body = 'to test maildir'
35 maildir_account.putmail(folder, mail)
36
37 assert_equal msgs + 1, message_count(folder)
38 end
39
40 def test_updatemail
41 folder = create_maildir
42 path = maildir_account.send(
43 :find_mails,
44 folder,
45 'regular-message-id@debian.org'
46 ).first
47 assert_not_nil path
48 mail = RMail::Message.new
49 mail.header['Subject'] = 'a different subject'
50 mail.header['Message-ID'] = 'regular-message-id@debian.org'
51 mail.body = 'This is the body of the message'
52 maildir_account.updatemail(folder, mail, 'regular-message-id@debian.org')
53
54 updated_path = maildir_account.send(
55 :find_mails,
56 folder,
57 'regular-message-id@debian.org'
58 ).first
59 updated_mail = RMail::Parser.read(File.open(File.join(folder, updated_path)))
60
61 assert_equal 'a different subject', updated_mail.header['Subject']
62 end
63
64 def test_find_mails
65 folder = create_maildir
66 assert_equal 0, maildir_account.send(:find_mails, folder, 'SomeRandomMessageID').size
67 end
68
69 private
70
71 def create_maildir
72 parent = Dir.mktmpdir
73 @tmpdirs << parent
74 FileUtils.cp_r('test/maildir', parent)
75 return File.join(parent, 'maildir')
76 end
77
78 def message_count(folder)
79 Dir.glob(File.join(folder, '**', '*')).reject { |f| File.directory?(f) }.size
80 end
81
82 def maildir_account
83 @maildir_account ||=
84 begin
85 MaildirAccount.new.tap do |account|
86 account.stubs(:puts)
87 end
88 end
89 end
90
91 end
92