rgrove-larch 1.0.0.7 → 1.0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|