rgrove-larch 1.0.0.7 → 1.0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/larch +27 -27
- data/lib/larch.rb +29 -101
- data/lib/larch/errors.rb +2 -1
- data/lib/larch/imap.rb +112 -299
- metadata +2 -2
data/bin/larch
CHANGED
@@ -24,18 +24,18 @@ EOS
|
|
24
24
|
|
25
25
|
text "\nCopy Options:"
|
26
26
|
opt :from_folder, "Source folder to copy from", :short => :none, :default => 'INBOX'
|
27
|
-
opt :from_pass, "Source server password (
|
28
|
-
opt :from_user, "Source server username (
|
27
|
+
opt :from_pass, "Source server password (default: prompt)", :short => :none, :type => :string
|
28
|
+
opt :from_user, "Source server username (default: prompt)", :short => :none, :type => :string
|
29
29
|
opt :to_folder, "Destination folder to copy to", :short => :none, :default => 'INBOX'
|
30
|
-
opt :to_pass, "Destination server password (
|
31
|
-
opt :to_user, "Destination server username (
|
30
|
+
opt :to_pass, "Destination server password (default: prompt)", :short => :none, :type => :string
|
31
|
+
opt :to_user, "Destination server username (default: prompt)", :short => :none, :type => :string
|
32
32
|
|
33
33
|
text "\nGeneral Options:"
|
34
34
|
# opt :dry_run, "Don't actually do anything.", :short => '-n'
|
35
35
|
opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
|
36
36
|
opt :max_retries, "Maximum number of times to retry after a recoverable error", :short => :none, :default => 3
|
37
37
|
opt :no_create_folder, "Don't create destination folders that don't already exist", :short => :none
|
38
|
-
opt :ssl_certs, "Path to a trusted certificate bundle to use to verify server SSL certificates", :short => :none
|
38
|
+
opt :ssl_certs, "Path to a trusted certificate bundle to use to verify server SSL certificates", :short => :none, :type => :string
|
39
39
|
opt :ssl_verify, "Verify server SSL certificates", :short => :none
|
40
40
|
opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal", :short => '-V', :default => 'info'
|
41
41
|
end
|
@@ -52,45 +52,45 @@ EOS
|
|
52
52
|
end
|
53
53
|
|
54
54
|
# Create URIs.
|
55
|
-
|
56
|
-
|
57
|
-
options[:from].path = '/' + CGI.escape(options[:from_folder].gsub(/^\//, ''))
|
58
|
-
options[:to].path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
|
59
|
-
|
60
|
-
# Prompt for usernames and passwords if necessary.
|
61
|
-
unless options[:from_user]
|
62
|
-
options[:from_user] = ask("Source username (#{options[:from].host}): ")
|
63
|
-
end
|
55
|
+
uri_from = URI(options[:from])
|
56
|
+
uri_to = URI(options[:to])
|
64
57
|
|
65
|
-
|
66
|
-
|
58
|
+
if options[:from_folder_given] || options[:to_folder_given]
|
59
|
+
uri_from.path = '/' + CGI.escape(options[:from_folder].gsub(/^\//, ''))
|
60
|
+
uri_to.path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
|
67
61
|
end
|
68
62
|
|
69
|
-
|
70
|
-
|
71
|
-
|
63
|
+
# Usernames and passwords specified as arguments override those in the URIs
|
64
|
+
uri_from.user = CGI.escape(options[:from_user]) if options[:from_user]
|
65
|
+
uri_from.password = CGI.escape(options[:from_pass]) if options[:from_pass]
|
66
|
+
uri_to.user = CGI.escape(options[:to_user]) if options[:to_user]
|
67
|
+
uri_to.password = CGI.escape(options[:to_pass]) if options[:to_pass]
|
72
68
|
|
73
|
-
|
74
|
-
|
75
|
-
|
69
|
+
# If usernames/passwords aren't specified in either URIs or args, then prompt.
|
70
|
+
uri_from.user ||= CGI.escape(ask("Source username (#{uri_from.host}): "))
|
71
|
+
uri_from.password ||= CGI.escape(ask("Source password (#{uri_from.host}): ") {|q| q.echo = false })
|
72
|
+
uri_to.user ||= CGI.escape(ask("Destination username (#{uri_to.host}): "))
|
73
|
+
uri_to.password ||= CGI.escape(ask("Destination password (#{uri_to.host}): ") {|q| q.echo = false })
|
76
74
|
|
77
75
|
# Go go go!
|
78
76
|
init(options[:verbosity])
|
79
77
|
|
80
78
|
Net::IMAP.debug = true if @log.level == :insane
|
81
79
|
|
82
|
-
|
80
|
+
imap_from = Larch::IMAP.new(uri_from,
|
83
81
|
:fast_scan => options[:fast_scan],
|
84
82
|
:max_retries => options[:max_retries],
|
85
83
|
:ssl_certs => options[:ssl_certs] || nil,
|
86
|
-
:ssl_verify => options[:ssl_verify]
|
84
|
+
:ssl_verify => options[:ssl_verify]
|
85
|
+
)
|
87
86
|
|
88
|
-
|
87
|
+
imap_to = Larch::IMAP.new(uri_to,
|
89
88
|
:create_mailbox => !options[:no_create_folder],
|
90
89
|
:fast_scan => options[:fast_scan],
|
91
90
|
:max_retries => options[:max_retries],
|
92
91
|
:ssl_certs => options[:ssl_certs] || nil,
|
93
|
-
:ssl_verify => options[:ssl_verify]
|
92
|
+
:ssl_verify => options[:ssl_verify]
|
93
|
+
)
|
94
94
|
|
95
95
|
unless RUBY_PLATFORM =~ /mswin|mingw|bccwin|wince|java/
|
96
96
|
begin
|
@@ -101,5 +101,5 @@ EOS
|
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
|
104
|
+
copy_folder(imap_from, imap_to)
|
105
105
|
end
|
data/lib/larch.rb
CHANGED
@@ -11,6 +11,7 @@ require 'uri'
|
|
11
11
|
|
12
12
|
require 'larch/errors'
|
13
13
|
require 'larch/imap'
|
14
|
+
require 'larch/imap/mailbox'
|
14
15
|
require 'larch/logger'
|
15
16
|
require 'larch/version'
|
16
17
|
|
@@ -27,128 +28,55 @@ module Larch
|
|
27
28
|
@total = 0
|
28
29
|
end
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
raise ArgumentError, "source must be a Larch::IMAP instance" unless source.is_a?(IMAP)
|
34
|
-
raise ArgumentError, "dest must be a Larch::IMAP instance" unless dest.is_a?(IMAP)
|
35
|
-
|
36
|
-
msgq = SizedQueue.new(8)
|
37
|
-
mutex = Mutex.new
|
31
|
+
def copy_folder(imap_from, imap_to)
|
32
|
+
raise ArgumentError, "source must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
33
|
+
raise ArgumentError, "dest must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
38
34
|
|
39
35
|
@copied = 0
|
40
36
|
@failed = 0
|
41
37
|
@total = 0
|
42
38
|
|
43
|
-
|
39
|
+
mailbox_from_name = imap_from.uri_mailbox || 'INBOX'
|
40
|
+
mailbox_to_name = imap_to.uri_mailbox || 'INBOX'
|
44
41
|
|
45
|
-
|
46
|
-
dest.connect
|
42
|
+
@log.info "copying messages from #{imap_from.host}/#{mailbox_from_name} to #{imap_to.host}/#{mailbox_to_name}"
|
47
43
|
|
48
|
-
|
49
|
-
|
50
|
-
source.scan_mailbox
|
51
|
-
mutex.synchronize { @total = source.length }
|
52
|
-
|
53
|
-
source.each do |id|
|
54
|
-
next if dest.has_message?(id)
|
55
|
-
|
56
|
-
begin
|
57
|
-
Thread.current[:fetching] = true
|
58
|
-
msgq << source.peek(id)
|
59
|
-
Thread.current[:fetching] = false
|
60
|
-
|
61
|
-
rescue Larch::IMAP::Error => e
|
62
|
-
# TODO: Keep failed message envelopes in a buffer for later output?
|
63
|
-
mutex.synchronize { @failed += 1 }
|
64
|
-
@log.error e.message
|
65
|
-
next
|
66
|
-
end
|
67
|
-
end
|
44
|
+
imap_from.connect
|
45
|
+
imap_to.connect
|
68
46
|
|
69
|
-
|
70
|
-
|
71
|
-
@log.debug "#{source.username}@#{source.host}: watchdog exception"
|
72
|
-
source.noop
|
73
|
-
retry
|
47
|
+
mailbox_from = imap_from.mailbox(mailbox_from_name)
|
48
|
+
mailbox_to = imap_to.mailbox(mailbox_to_name)
|
74
49
|
|
75
|
-
|
76
|
-
@log.fatal "#{source.username}@#{source.host}: #{e.class.name}: #{e.message}"
|
77
|
-
Kernel.abort
|
50
|
+
@total = mailbox_from.length
|
78
51
|
|
79
|
-
|
80
|
-
|
81
|
-
end
|
82
|
-
end
|
52
|
+
mailbox_from.each do |id|
|
53
|
+
next if mailbox_to.has_message?(id)
|
83
54
|
|
84
|
-
dest_thread = Thread.new do
|
85
55
|
begin
|
86
|
-
|
56
|
+
msg = mailbox_from.peek(id)
|
87
57
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
else
|
95
|
-
from = '?'
|
96
|
-
end
|
58
|
+
if msg.envelope.from
|
59
|
+
env_from = msg.envelope.from.first
|
60
|
+
from = "#{env_from.mailbox}@#{env_from.host}"
|
61
|
+
else
|
62
|
+
from = '?'
|
63
|
+
end
|
97
64
|
|
98
|
-
|
65
|
+
@log.info "copying message: #{from} - #{msg.envelope.subject}"
|
99
66
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
mutex.synchronize { @copied += 1 }
|
104
|
-
end
|
67
|
+
mailbox_to << msg
|
68
|
+
@copied += 1
|
105
69
|
|
106
70
|
rescue Larch::IMAP::Error => e
|
107
|
-
|
71
|
+
# TODO: Keep failed message envelopes in a buffer for later output?
|
72
|
+
@failed += 1
|
108
73
|
@log.error e.message
|
109
|
-
|
110
|
-
|
111
|
-
rescue Larch::WatchdogException => e
|
112
|
-
Thread.current[:last_id] = nil
|
113
|
-
@log.debug "#{dest.username}@#{dest.host}: watchdog exception"
|
114
|
-
dest.noop
|
115
|
-
retry
|
116
|
-
|
117
|
-
rescue => e
|
118
|
-
@log.fatal "#{dest.username}@#{dest.host}: #{e.class.name}: #{e.message}"
|
119
|
-
Kernel.abort
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
watchdog_thread = Thread.new do
|
124
|
-
source_flags = 0
|
125
|
-
dest_flags = 0
|
126
|
-
dest_lastid = nil
|
127
|
-
|
128
|
-
loop do
|
129
|
-
sleep 10
|
130
|
-
|
131
|
-
if msgq.length == 0 && source_thread[:fetching] && (source_flags += 1) > 1
|
132
|
-
source_flags = 0
|
133
|
-
source_thread.raise(WatchdogException)
|
134
|
-
end
|
135
|
-
|
136
|
-
if dest_thread[:last_id]
|
137
|
-
if dest_lastid == dest_thread[:last_id] && (dest_flags += 1) > 2
|
138
|
-
dest_flags = 0
|
139
|
-
dest_lastid = nil
|
140
|
-
dest_thread.raise(WatchdogException)
|
141
|
-
else
|
142
|
-
dest_lastid = dest_thread[:last_id]
|
143
|
-
end
|
144
|
-
end
|
74
|
+
next
|
145
75
|
end
|
146
76
|
end
|
147
77
|
|
148
|
-
|
149
|
-
|
150
|
-
source.disconnect
|
151
|
-
dest.disconnect
|
78
|
+
imap_from.disconnect
|
79
|
+
imap_to.disconnect
|
152
80
|
|
153
81
|
rescue => e
|
154
82
|
@log.fatal e.message
|
data/lib/larch/errors.rb
CHANGED
data/lib/larch/imap.rb
CHANGED
@@ -6,34 +6,23 @@ module Larch
|
|
6
6
|
# required reading if you're doing anything with IMAP in Ruby:
|
7
7
|
# http://sup.rubyforge.org
|
8
8
|
class IMAP
|
9
|
-
attr_reader :
|
10
|
-
|
11
|
-
# Maximum number of messages to fetch at once.
|
12
|
-
MAX_FETCH_COUNT = 1024
|
13
|
-
|
14
|
-
# Regex to capture the individual fields in an IMAP fetch command.
|
15
|
-
REGEX_FIELDS = /([0-9A-Z\.]+\[[^\]]+\](?:<[0-9\.]+>)?|[0-9A-Z\.]+)/
|
16
|
-
|
17
|
-
# Regex to capture a Message-Id header.
|
18
|
-
REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
|
9
|
+
attr_reader :conn, :options
|
19
10
|
|
20
11
|
# URI format validation regex.
|
21
12
|
REGEX_URI = URI.regexp(['imap', 'imaps'])
|
22
13
|
|
23
|
-
# Minimum time (in seconds) allowed between mailbox scans.
|
24
|
-
SCAN_INTERVAL = 60
|
25
|
-
|
26
14
|
# Larch::IMAP::Message represents a transferable IMAP message which can be
|
27
15
|
# passed between Larch::IMAP instances.
|
28
16
|
Message = Struct.new(:id, :envelope, :rfc822, :flags, :internaldate)
|
29
17
|
|
30
18
|
# Initializes a new Larch::IMAP instance that will connect to the specified
|
31
|
-
# IMAP URI
|
19
|
+
# IMAP URI.
|
32
20
|
#
|
33
|
-
#
|
21
|
+
# In addition to the URI, the following options may also be specified:
|
34
22
|
#
|
35
23
|
# [:create_mailbox]
|
36
|
-
# If +true+,
|
24
|
+
# If +true+, mailboxes that don't already exist will be created if
|
25
|
+
# necessary.
|
37
26
|
#
|
38
27
|
# [:fast_scan]
|
39
28
|
# If +true+, a faster but less accurate method will be used to scan
|
@@ -58,32 +47,25 @@ class IMAP
|
|
58
47
|
# certificate bundle specified in +ssl_certs+. By default, server SSL
|
59
48
|
# certificates are not verified.
|
60
49
|
#
|
61
|
-
def initialize(uri,
|
50
|
+
def initialize(uri, options = {})
|
62
51
|
raise ArgumentError, "not an IMAP URI: #{uri}" unless uri.is_a?(URI) || uri =~ REGEX_URI
|
63
|
-
raise ArgumentError, "must provide a username and password" unless username && password
|
64
52
|
raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
|
65
53
|
|
66
|
-
@
|
67
|
-
@
|
68
|
-
@password = password
|
69
|
-
@options = {:max_retries => 3, :ssl_verify => false}.merge(options)
|
54
|
+
@options = {:max_retries => 3, :ssl_verify => false}.merge(options)
|
55
|
+
@uri = uri.is_a?(URI) ? uri : URI(uri)
|
70
56
|
|
71
|
-
@
|
72
|
-
@imap = nil
|
73
|
-
@last_id = 0
|
74
|
-
@last_scan = nil
|
75
|
-
@mutex = Mutex.new
|
57
|
+
raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password
|
76
58
|
|
77
|
-
|
78
|
-
|
79
|
-
@
|
59
|
+
@conn = nil
|
60
|
+
@mailboxes = {}
|
61
|
+
@mutex = Mutex.new
|
80
62
|
|
81
63
|
# Create private convenience methods (debug, info, warn, etc.) to make
|
82
64
|
# logging easier.
|
83
65
|
Logger::LEVELS.each_key do |level|
|
84
66
|
IMAP.class_eval do
|
85
67
|
define_method(level) do |msg|
|
86
|
-
Larch.log.log(level, "#{
|
68
|
+
Larch.log.log(level, "#{username}@#{host}: #{msg}")
|
87
69
|
end
|
88
70
|
|
89
71
|
private level
|
@@ -91,37 +73,19 @@ class IMAP
|
|
91
73
|
end
|
92
74
|
end
|
93
75
|
|
94
|
-
# Appends the specified Larch::IMAP::Message to this mailbox if it doesn't
|
95
|
-
# already exist. Returns +true+ if the message was appended successfully,
|
96
|
-
# +false+ if the message already exists in the mailbox.
|
97
|
-
def append(message)
|
98
|
-
raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Message)
|
99
|
-
return false if has_message?(message)
|
100
|
-
|
101
|
-
safely do
|
102
|
-
imap_select(!!@options[:create_mailbox])
|
103
|
-
|
104
|
-
debug "appending message: #{message.id}"
|
105
|
-
@imap.append(mailbox, message.rfc822, message.flags, message.internaldate)
|
106
|
-
end
|
107
|
-
|
108
|
-
true
|
109
|
-
end
|
110
|
-
alias << append
|
111
|
-
|
112
76
|
# Connects to the IMAP server and logs in if a connection hasn't already been
|
113
77
|
# established.
|
114
78
|
def connect
|
115
|
-
return if @
|
79
|
+
return if @conn
|
116
80
|
safely {} # connect, but do nothing else
|
117
81
|
end
|
118
82
|
|
119
83
|
# Closes the IMAP connection if one is currently open.
|
120
84
|
def disconnect
|
121
|
-
return unless @
|
85
|
+
return unless @conn
|
122
86
|
|
123
87
|
begin
|
124
|
-
@
|
88
|
+
@conn.disconnect
|
125
89
|
rescue Errno::ENOTCONN => e
|
126
90
|
debug "#{e.class.name}: #{e.message}"
|
127
91
|
end
|
@@ -131,47 +95,11 @@ class IMAP
|
|
131
95
|
info "disconnected"
|
132
96
|
end
|
133
97
|
|
134
|
-
# Iterates through
|
135
|
-
#
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
ids.each_key {|id| yield id }
|
141
|
-
end
|
142
|
-
|
143
|
-
# Gets a Net::IMAP::Envelope for the specified message id.
|
144
|
-
def envelope(message_id)
|
145
|
-
scan_mailbox
|
146
|
-
uid = @ids[message_id]
|
147
|
-
|
148
|
-
raise NotFoundError, "message not found: #{message_id}" if uid.nil?
|
149
|
-
|
150
|
-
debug "fetching envelope: #{message_id}"
|
151
|
-
imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
152
|
-
end
|
153
|
-
|
154
|
-
# Fetches a Larch::IMAP::Message struct representing the message with the
|
155
|
-
# specified Larch message id.
|
156
|
-
def fetch(message_id, peek = false)
|
157
|
-
scan_mailbox
|
158
|
-
uid = @ids[message_id]
|
159
|
-
|
160
|
-
raise NotFoundError, "message not found: #{message_id}" if uid.nil?
|
161
|
-
|
162
|
-
debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
|
163
|
-
data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
|
164
|
-
|
165
|
-
Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
|
166
|
-
data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
|
167
|
-
end
|
168
|
-
alias [] fetch
|
169
|
-
|
170
|
-
# Returns +true+ if a message with the specified Larch <em>message_id</em>
|
171
|
-
# exists in this mailbox, +false+ otherwise.
|
172
|
-
def has_message?(message_id)
|
173
|
-
scan_mailbox
|
174
|
-
@ids.has_key?(message_id)
|
98
|
+
# Iterates through all mailboxes in the account, yielding each one as a
|
99
|
+
# Larch::IMAP::Mailbox instance to the given block.
|
100
|
+
def each_mailbox
|
101
|
+
update_mailboxes
|
102
|
+
@mailboxes.each_value {|mailbox| yield mailbox }
|
175
103
|
end
|
176
104
|
|
177
105
|
# Gets the IMAP hostname.
|
@@ -179,26 +107,39 @@ class IMAP
|
|
179
107
|
@uri.host
|
180
108
|
end
|
181
109
|
|
182
|
-
# Gets
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
110
|
+
# Gets a Larch::IMAP::Mailbox instance representing the specified mailbox. If
|
111
|
+
# the mailbox doesn't exist and the +:create_mailbox+ option is +false+, or if
|
112
|
+
# +:create_mailbox+ is +true+ and mailbox creation fails, a
|
113
|
+
# Larch::IMAP::MailboxNotFoundError will be raised.
|
114
|
+
def mailbox(name)
|
115
|
+
retries = 0
|
188
116
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
117
|
+
begin
|
118
|
+
@mailboxes.fetch(name) do
|
119
|
+
update_mailboxes
|
120
|
+
return @mailboxes[name] if @mailboxes.has_key?(name)
|
121
|
+
raise MailboxNotFoundError, "mailbox not found: #{name}"
|
122
|
+
end
|
123
|
+
|
124
|
+
rescue MailboxNotFoundError => e
|
125
|
+
raise unless @options[:create_mailbox] && retries == 0
|
126
|
+
|
127
|
+
info "creating mailbox: #{name}"
|
128
|
+
safely { @conn.create(name) }
|
129
|
+
|
130
|
+
retries += 1
|
131
|
+
retry
|
132
|
+
end
|
193
133
|
end
|
194
134
|
|
135
|
+
# Sends an IMAP NOOP command.
|
195
136
|
def noop
|
196
|
-
safely { @
|
137
|
+
safely { @conn.noop }
|
197
138
|
end
|
198
139
|
|
199
|
-
#
|
200
|
-
def
|
201
|
-
|
140
|
+
# Gets the IMAP password.
|
141
|
+
def password
|
142
|
+
CGI.unescape(@uri.password)
|
202
143
|
end
|
203
144
|
|
204
145
|
# Gets the IMAP port number.
|
@@ -206,50 +147,52 @@ class IMAP
|
|
206
147
|
@uri.port || (ssl? ? 993 : 143)
|
207
148
|
end
|
208
149
|
|
209
|
-
#
|
210
|
-
|
211
|
-
|
150
|
+
# Connect if necessary, execute the given block, retry up to 3 times if a
|
151
|
+
# recoverable error occurs, die if an unrecoverable error occurs.
|
152
|
+
def safely
|
153
|
+
safe_connect
|
154
|
+
|
155
|
+
retries = 0
|
212
156
|
|
213
157
|
begin
|
214
|
-
|
215
|
-
rescue Error => e
|
216
|
-
return if @options[:create_mailbox]
|
217
|
-
raise
|
218
|
-
end
|
158
|
+
yield
|
219
159
|
|
220
|
-
|
160
|
+
rescue Errno::ECONNRESET,
|
161
|
+
Errno::ENOTCONN,
|
162
|
+
Errno::EPIPE,
|
163
|
+
Errno::ETIMEDOUT,
|
164
|
+
Net::IMAP::ByeResponseError,
|
165
|
+
OpenSSL::SSL::SSLError => e
|
221
166
|
|
222
|
-
|
223
|
-
return if last_id == @last_id
|
167
|
+
raise unless (retries += 1) <= @options[:max_retries]
|
224
168
|
|
225
|
-
|
169
|
+
info "#{e.class.name}: #{e.message} (reconnecting)"
|
226
170
|
|
227
|
-
|
171
|
+
reset
|
172
|
+
sleep 1 * retries
|
173
|
+
safe_connect
|
174
|
+
retry
|
228
175
|
|
229
|
-
|
230
|
-
|
176
|
+
rescue Net::IMAP::BadResponseError,
|
177
|
+
Net::IMAP::NoResponseError,
|
178
|
+
Net::IMAP::ResponseParseError => e
|
231
179
|
|
232
|
-
|
233
|
-
['UID', 'RFC822.SIZE', 'INTERNALDATE']
|
234
|
-
else
|
235
|
-
"(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
|
236
|
-
end
|
180
|
+
raise unless (retries += 1) <= @options[:max_retries]
|
237
181
|
|
238
|
-
|
239
|
-
id = create_id(data)
|
182
|
+
info "#{e.class.name}: #{e.message} (will retry)"
|
240
183
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
end
|
184
|
+
sleep 1 * retries
|
185
|
+
retry
|
186
|
+
end
|
245
187
|
|
246
|
-
|
247
|
-
|
248
|
-
debug "duplicate message? #{id} (Subject: #{envelope.subject})"
|
249
|
-
end
|
188
|
+
rescue Larch::Error => e
|
189
|
+
raise
|
250
190
|
|
251
|
-
|
252
|
-
|
191
|
+
rescue Net::IMAP::Error => e
|
192
|
+
raise Error, "#{e.class.name}: #{e.message} (giving up)"
|
193
|
+
|
194
|
+
rescue => e
|
195
|
+
raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
|
253
196
|
end
|
254
197
|
|
255
198
|
# Gets the SSL status.
|
@@ -262,126 +205,29 @@ class IMAP
|
|
262
205
|
@uri.to_s
|
263
206
|
end
|
264
207
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
#
|
270
|
-
# If the given message data includes a valid Message-Id header, then that will
|
271
|
-
# be used to generate an MD5 hash. Otherwise, the hash will be generated based
|
272
|
-
# on the message's RFC822.SIZE and INTERNALDATE.
|
273
|
-
def create_id(data)
|
274
|
-
['RFC822.SIZE', 'INTERNALDATE'].each do |a|
|
275
|
-
raise FatalError, "requested data not in IMAP response: #{a}" unless data.attr[a]
|
276
|
-
end
|
277
|
-
|
278
|
-
if data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'] =~ REGEX_MESSAGE_ID
|
279
|
-
Digest::MD5.hexdigest($1)
|
280
|
-
else
|
281
|
-
Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
|
282
|
-
Time.parse(data.attr['INTERNALDATE']).to_i))
|
283
|
-
end
|
284
|
-
end
|
285
|
-
|
286
|
-
# Examines the mailbox. If _force_ is true, the mailbox will be examined even
|
287
|
-
# if it is already selected (which isn't necessary unless you want to ensure
|
288
|
-
# that it's in a read-only state).
|
289
|
-
def imap_examine(force = false)
|
290
|
-
return if @mailbox_state == :examined || (!force && @mailbox_state == :selected)
|
291
|
-
|
292
|
-
safely do
|
293
|
-
begin
|
294
|
-
@mutex.synchronize { @mailbox_state = :closed }
|
295
|
-
|
296
|
-
debug "examining mailbox: #{mailbox}"
|
297
|
-
@imap.examine(mailbox)
|
298
|
-
|
299
|
-
@mutex.synchronize { @mailbox_state = :examined }
|
300
|
-
|
301
|
-
rescue Net::IMAP::NoResponseError => e
|
302
|
-
raise Error, "unable to examine mailbox: #{e.message}"
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
# Fetches the specified _fields_ for the specified message sequence id(s) from
|
308
|
-
# the IMAP server.
|
309
|
-
def imap_fetch(ids, fields)
|
310
|
-
ids = ids.to_a
|
311
|
-
data = []
|
312
|
-
pos = 0
|
313
|
-
|
314
|
-
while pos < ids.length
|
315
|
-
safely do
|
316
|
-
imap_examine
|
317
|
-
|
318
|
-
data += @imap.fetch(ids[pos, MAX_FETCH_COUNT], fields)
|
319
|
-
pos += MAX_FETCH_COUNT
|
320
|
-
end
|
321
|
-
end
|
322
|
-
|
323
|
-
data
|
208
|
+
# Gets the IMAP mailbox specified in the URI, or +nil+ if none.
|
209
|
+
def uri_mailbox
|
210
|
+
mb = @uri.path[1..-1]
|
211
|
+
mb.nil? || mb.empty? ? nil : CGI.unescape(mb)
|
324
212
|
end
|
325
213
|
|
326
|
-
#
|
327
|
-
|
328
|
-
|
329
|
-
def imap_select(create = false)
|
330
|
-
return if @mailbox_state == :selected
|
331
|
-
|
332
|
-
safely do
|
333
|
-
begin
|
334
|
-
@mutex.synchronize { @mailbox_state = :closed }
|
335
|
-
|
336
|
-
debug "selecting mailbox: #{mailbox}"
|
337
|
-
@imap.select(mailbox)
|
338
|
-
|
339
|
-
@mutex.synchronize { @mailbox_state = :selected }
|
340
|
-
|
341
|
-
rescue Net::IMAP::NoResponseError => e
|
342
|
-
raise Error, "unable to select mailbox: #{e.message}" unless create
|
343
|
-
|
344
|
-
info "creating mailbox: #{mailbox}"
|
345
|
-
|
346
|
-
begin
|
347
|
-
@imap.create(mailbox)
|
348
|
-
retry
|
349
|
-
rescue => e
|
350
|
-
raise Error, "unable to create mailbox: #{e.message}"
|
351
|
-
end
|
352
|
-
end
|
353
|
-
end
|
214
|
+
# Gets the IMAP username.
|
215
|
+
def username
|
216
|
+
CGI.unescape(@uri.user)
|
354
217
|
end
|
355
218
|
|
356
|
-
|
357
|
-
# server.
|
358
|
-
def imap_uid_fetch(uids, fields)
|
359
|
-
uids = uids.to_a
|
360
|
-
data = []
|
361
|
-
pos = 0
|
362
|
-
|
363
|
-
while pos < uids.length
|
364
|
-
safely do
|
365
|
-
imap_examine
|
366
|
-
|
367
|
-
data += @imap.uid_fetch(uids[pos, MAX_FETCH_COUNT], fields)
|
368
|
-
pos += MAX_FETCH_COUNT
|
369
|
-
end
|
370
|
-
end
|
371
|
-
|
372
|
-
data
|
373
|
-
end
|
219
|
+
private
|
374
220
|
|
375
221
|
# Resets the connection and mailbox state.
|
376
222
|
def reset
|
377
223
|
@mutex.synchronize do
|
378
|
-
@
|
379
|
-
@
|
224
|
+
@conn = nil
|
225
|
+
@mailboxes.each_value {|mb| mb.reset }
|
380
226
|
end
|
381
227
|
end
|
382
228
|
|
383
229
|
def safe_connect
|
384
|
-
return if @
|
230
|
+
return if @conn
|
385
231
|
|
386
232
|
retries = 0
|
387
233
|
|
@@ -410,54 +256,6 @@ class IMAP
|
|
410
256
|
raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
|
411
257
|
end
|
412
258
|
|
413
|
-
# Connect if necessary, execute the given block, retry up to 3 times if a
|
414
|
-
# recoverable error occurs, die if an unrecoverable error occurs.
|
415
|
-
def safely
|
416
|
-
safe_connect
|
417
|
-
|
418
|
-
retries = 0
|
419
|
-
|
420
|
-
begin
|
421
|
-
yield
|
422
|
-
|
423
|
-
rescue Errno::ECONNRESET,
|
424
|
-
Errno::ENOTCONN,
|
425
|
-
Errno::EPIPE,
|
426
|
-
Errno::ETIMEDOUT,
|
427
|
-
Net::IMAP::ByeResponseError,
|
428
|
-
OpenSSL::SSL::SSLError => e
|
429
|
-
|
430
|
-
raise unless (retries += 1) <= @options[:max_retries]
|
431
|
-
|
432
|
-
info "#{e.class.name}: #{e.message} (reconnecting)"
|
433
|
-
|
434
|
-
reset
|
435
|
-
sleep 1 * retries
|
436
|
-
safe_connect
|
437
|
-
retry
|
438
|
-
|
439
|
-
rescue Net::IMAP::BadResponseError,
|
440
|
-
Net::IMAP::NoResponseError,
|
441
|
-
Net::IMAP::ResponseParseError => e
|
442
|
-
|
443
|
-
raise unless (retries += 1) <= @options[:max_retries]
|
444
|
-
|
445
|
-
info "#{e.class.name}: #{e.message} (will retry)"
|
446
|
-
|
447
|
-
sleep 1 * retries
|
448
|
-
retry
|
449
|
-
end
|
450
|
-
|
451
|
-
rescue Larch::Error => e
|
452
|
-
raise
|
453
|
-
|
454
|
-
rescue Net::IMAP::Error => e
|
455
|
-
raise Error, "#{e.class.name}: #{e.message} (giving up)"
|
456
|
-
|
457
|
-
rescue => e
|
458
|
-
raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
|
459
|
-
end
|
460
|
-
|
461
259
|
def unsafe_connect
|
462
260
|
info "connecting..."
|
463
261
|
|
@@ -465,7 +263,7 @@ class IMAP
|
|
465
263
|
|
466
264
|
Thread.new do
|
467
265
|
begin
|
468
|
-
@
|
266
|
+
@conn = Net::IMAP.new(host, port, ssl?,
|
469
267
|
ssl? && @options[:ssl_verify] ? @options[:ssl_certs] : nil,
|
470
268
|
@options[:ssl_verify])
|
471
269
|
|
@@ -473,7 +271,7 @@ class IMAP
|
|
473
271
|
|
474
272
|
auth_methods = ['PLAIN']
|
475
273
|
tried = []
|
476
|
-
capability = @
|
274
|
+
capability = @conn.capability
|
477
275
|
|
478
276
|
['LOGIN', 'CRAM-MD5'].each do |method|
|
479
277
|
auth_methods << method if capability.include?("AUTH=#{method}")
|
@@ -485,9 +283,9 @@ class IMAP
|
|
485
283
|
debug "authenticating using #{method}"
|
486
284
|
|
487
285
|
if method == 'PLAIN'
|
488
|
-
@
|
286
|
+
@conn.login(username, password)
|
489
287
|
else
|
490
|
-
@
|
288
|
+
@conn.authenticate(method, username, password)
|
491
289
|
end
|
492
290
|
|
493
291
|
info "authenticated using #{method}"
|
@@ -506,6 +304,21 @@ class IMAP
|
|
506
304
|
|
507
305
|
raise exception if exception
|
508
306
|
end
|
307
|
+
|
308
|
+
def update_mailboxes
|
309
|
+
list = safely { @conn.list('', '*') }
|
310
|
+
|
311
|
+
@mutex.synchronize do
|
312
|
+
# Remove cached mailboxes that no longer exist.
|
313
|
+
@mailboxes.delete_if {|k, v| !list.any?{|mb| mb.name == k}}
|
314
|
+
|
315
|
+
# Update cached mailboxes.
|
316
|
+
list.each do |mb|
|
317
|
+
@mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim, mb.attr)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
509
322
|
end
|
510
323
|
|
511
324
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rgrove-larch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Grove
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-04-08 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|