larch 1.0.0 → 1.0.1
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/HISTORY +23 -0
- data/README.rdoc +185 -0
- data/bin/larch +91 -34
- data/lib/larch/errors.rb +2 -1
- data/lib/larch/imap/mailbox.rb +279 -0
- data/lib/larch/imap.rb +167 -246
- data/lib/larch/logger.rb +6 -5
- data/lib/larch/version.rb +1 -1
- data/lib/larch.rb +142 -73
- metadata +9 -5
- data/lib/larch/util.rb +0 -17
data/HISTORY
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Larch History
|
2
|
+
================================================================================
|
3
|
+
|
4
|
+
Version 1.0.1 (2009-05-10)
|
5
|
+
* Ruby 1.9.1 support.
|
6
|
+
* Much more robust handling of unexpected server disconnects and dropped
|
7
|
+
connections.
|
8
|
+
* Added --all option to copy all folders recursively.
|
9
|
+
* Added --all-subscribed option to copy all subscribed folders recursively.
|
10
|
+
* Added --dry-run option to simulate changes without actually making them.
|
11
|
+
* Added --exclude and --exclude-file options to specify folders that should
|
12
|
+
not be copied.
|
13
|
+
* Added --ssl-certs option to specify a bundle of trusted SSL certificates.
|
14
|
+
* Added --ssl-verify option to verify server SSL certificates.
|
15
|
+
* Added a new "insane" logging level, which will output all IMAP commands and
|
16
|
+
responses to STDERR.
|
17
|
+
* Fixed excessive post-scan processing times for very large mailboxes.
|
18
|
+
* Fixed potential scan problems with very large mailboxes on certain servers.
|
19
|
+
* POSIX signals are no longer trapped on platforms that aren't likely to
|
20
|
+
support them.
|
21
|
+
|
22
|
+
Version 1.0.0 (2009-03-17)
|
23
|
+
* First release.
|
data/README.rdoc
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
= Larch
|
2
|
+
|
3
|
+
Larch is a tool to copy messages from one IMAP server to another quickly and
|
4
|
+
safely. It's smart enough not to copy messages that already exist on the
|
5
|
+
destination and robust enough to deal with ornery or misbehaving servers.
|
6
|
+
|
7
|
+
Larch is particularly well-suited for copying email to, from, or between Gmail
|
8
|
+
accounts.
|
9
|
+
|
10
|
+
*Author*:: Ryan Grove (mailto:ryan@wonko.com)
|
11
|
+
*Version*:: 1.0.1 (2009-05-10)
|
12
|
+
*Copyright*:: Copyright (c) 2009 Ryan Grove. All rights reserved.
|
13
|
+
*License*:: GPL 2.0 (http://opensource.org/licenses/gpl-2.0.php)
|
14
|
+
*Website*:: http://github.com/rgrove/larch
|
15
|
+
|
16
|
+
== Installation
|
17
|
+
|
18
|
+
Install Larch via RubyGems:
|
19
|
+
|
20
|
+
gem install larch
|
21
|
+
|
22
|
+
== Usage
|
23
|
+
|
24
|
+
larch --from <uri> --to <uri> [options]
|
25
|
+
|
26
|
+
Required:
|
27
|
+
--from, -f <s>: URI of the source IMAP server.
|
28
|
+
--to, -t <s>: URI of the destination IMAP server.
|
29
|
+
|
30
|
+
Copy Options:
|
31
|
+
--all: Copy all folders recursively
|
32
|
+
--all-subscribed: Copy all subscribed folders recursively
|
33
|
+
--exclude <s+>: List of mailbox names/patterns that shouldn't be
|
34
|
+
copied
|
35
|
+
--exclude-file <s>: Filename containing mailbox names/patterns that
|
36
|
+
shouldn't be copied
|
37
|
+
--from-folder <s>: Source folder to copy from (default: INBOX)
|
38
|
+
--from-pass <s>: Source server password (default: prompt)
|
39
|
+
--from-user <s>: Source server username (default: prompt)
|
40
|
+
--to-folder <s>: Destination folder to copy to (default: INBOX)
|
41
|
+
--to-pass <s>: Destination server password (default: prompt)
|
42
|
+
--to-user <s>: Destination server username (default: prompt)
|
43
|
+
|
44
|
+
General Options:
|
45
|
+
--dry-run, -n: Don't actually make any changes.
|
46
|
+
--fast-scan: Use a faster (but less accurate) method to scan
|
47
|
+
mailboxes. This may result in messages being
|
48
|
+
re-copied.
|
49
|
+
--max-retries <i>: Maximum number of times to retry after a recoverable
|
50
|
+
error (default: 3)
|
51
|
+
--no-create-folder: Don't create destination folders that don't already
|
52
|
+
exist
|
53
|
+
--ssl-certs <s>: Path to a trusted certificate bundle to use to
|
54
|
+
verify server SSL certificates
|
55
|
+
--ssl-verify: Verify server SSL certificates
|
56
|
+
--verbosity, -V <s>: Output verbosity: debug, info, warn, error, or fatal
|
57
|
+
(default: info)
|
58
|
+
--version, -v: Print version and exit
|
59
|
+
--help, -h: Show this message
|
60
|
+
|
61
|
+
== Examples
|
62
|
+
|
63
|
+
Larch is run from the command line. At a minimum, you must specify a source
|
64
|
+
server and a destination server in the form of IMAP URIs. You may also specify
|
65
|
+
one or more options:
|
66
|
+
|
67
|
+
larch --from <uri> --to <uri> [options]
|
68
|
+
|
69
|
+
For an overview of all available command-line options, run:
|
70
|
+
|
71
|
+
larch --help
|
72
|
+
|
73
|
+
Specify a source server and a destination server and Larch will prompt you for
|
74
|
+
the necessary usernames and passwords, then sync the contents of the source's
|
75
|
+
+INBOX+ folder to the destination:
|
76
|
+
|
77
|
+
larch --from imap://mail.example.com --to imap://imap.gmail.com
|
78
|
+
|
79
|
+
To <b>connect using SSL</b>, specify a URI beginning with <tt>imaps://</tt>:
|
80
|
+
|
81
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com
|
82
|
+
|
83
|
+
If you'd like to <b>sync a specific folder</b> other than +INBOX+, specify the
|
84
|
+
source and destination folders using <tt>--from-folder</tt> and
|
85
|
+
<tt>--to-folder</tt>:
|
86
|
+
|
87
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com \
|
88
|
+
--from-folder "Sent Mail" --to-folder "Sent Mail"
|
89
|
+
|
90
|
+
To <b>sync all folders</b>, use the <tt>--all</tt> option (or
|
91
|
+
<tt>--all-subscribed</tt> if you only want to <b>sync subscribed folders</b>):
|
92
|
+
|
93
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all
|
94
|
+
|
95
|
+
By default Larch will create folders on the destination server if they don't
|
96
|
+
already exist. To prevent this, add the <tt>--no-create-folder</tt> option:
|
97
|
+
|
98
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all \
|
99
|
+
--no-create-folder
|
100
|
+
|
101
|
+
You can <b>prevent Larch from syncing one or more folders</b> by using the
|
102
|
+
<tt>--exclude</tt> option:
|
103
|
+
|
104
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all \
|
105
|
+
--exclude Spam Trash Drafts "[Gmail]/*"
|
106
|
+
|
107
|
+
If your exclusion list is long or complex, create a text file with one exclusion
|
108
|
+
pattern per line and tell Larch to load it with the <tt>--exclude-file</tt>
|
109
|
+
option:
|
110
|
+
|
111
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all \
|
112
|
+
--exclude-file exclude.txt
|
113
|
+
|
114
|
+
The wildcard characters <tt>*</tt> and <tt>?</tt> are supported in exclusion
|
115
|
+
lists. You can also use a regular expression by enclosing a pattern in
|
116
|
+
forward slashes, so the previous example could be achieved with the
|
117
|
+
pattern <tt>/(Spam|Trash|Drafts|\[Gmail\]\/.*)/</tt>
|
118
|
+
|
119
|
+
== Server Compatibility
|
120
|
+
|
121
|
+
Larch should work well with any server that properly supports
|
122
|
+
IMAP4rev1[http://tools.ietf.org/html/rfc3501], and does its best to get along
|
123
|
+
with servers that have buggy, unreliable, or incomplete IMAP implementations.
|
124
|
+
|
125
|
+
Larch has been tested on and is known to work well with the following IMAP
|
126
|
+
servers:
|
127
|
+
|
128
|
+
* Dovecot
|
129
|
+
* Gmail
|
130
|
+
* Microsoft Exchange 2003
|
131
|
+
|
132
|
+
== Known Issues
|
133
|
+
|
134
|
+
* Larch uses Ruby's Net::IMAP standard library for all IMAP operations. While
|
135
|
+
Net::IMAP is generally a very solid library, it contains a bug that can
|
136
|
+
cause a deadlock to occur if a connection drops unexpectedly (either due to
|
137
|
+
network issues or because the server closed the connection without warning)
|
138
|
+
when the server has already begun sending a response and Net::IMAP is
|
139
|
+
waiting to receive more data.
|
140
|
+
|
141
|
+
If this happens, Net::IMAP will continue waiting forever without passing
|
142
|
+
control back to Larch, and you will need to manually kill and restart Larch.
|
143
|
+
|
144
|
+
* The Ruby package on Debian, Ubuntu, and some other Debian-based Linux
|
145
|
+
distributions doesn't include the OpenSSL standard library. If you see an
|
146
|
+
error like <tt>uninitialized constant Larch::IMAP::OpenSSL (NameError)</tt>
|
147
|
+
when running Larch, you may need to install the <tt>libopenssl-ruby</tt>
|
148
|
+
package. Please feel free to complain to the maintainer of your distribution's
|
149
|
+
Ruby packages.
|
150
|
+
|
151
|
+
== Support
|
152
|
+
|
153
|
+
The Larch mailing list is the best place for questions, comments, and discussion
|
154
|
+
about Larch. You can join the list or view the archives at
|
155
|
+
http://groups.google.com/group/larch
|
156
|
+
|
157
|
+
== Credit
|
158
|
+
|
159
|
+
The Larch::IMAP class borrows heavily from Sup[http://sup.rubyforge.org] by
|
160
|
+
William Morgan, the source code of which should be required reading if you're
|
161
|
+
doing anything with IMAP in Ruby.
|
162
|
+
|
163
|
+
Larch uses the excellent Trollop[http://trollop.rubyforge.org] command-line
|
164
|
+
option parser (also by William Morgan) and the
|
165
|
+
HighLine[http://highline.rubyforge.org] command-line IO library (by James Edward
|
166
|
+
Gray II).
|
167
|
+
|
168
|
+
== License
|
169
|
+
|
170
|
+
Copyright (c) 2009 Ryan Grove <ryan@wonko.com>
|
171
|
+
|
172
|
+
Licensed under the GNU General Public License version 2.0.
|
173
|
+
|
174
|
+
This program is free software; you can redistribute it and/or modify it under
|
175
|
+
the terms of version 2.0 of the GNU General Public License as published by the
|
176
|
+
Free Software Foundation.
|
177
|
+
|
178
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
179
|
+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
180
|
+
PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
181
|
+
|
182
|
+
You should have received a copy of the GNU General Public License along with
|
183
|
+
this program; if not, visit http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
|
184
|
+
or write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
185
|
+
Boston, MA 02111-1307 USA.
|
data/bin/larch
CHANGED
@@ -22,18 +22,25 @@ EOS
|
|
22
22
|
opt :from, "URI of the source IMAP server.", :short => '-f', :type => :string, :required => true
|
23
23
|
opt :to, "URI of the destination IMAP server.", :short => '-t', :type => :string, :required => true
|
24
24
|
|
25
|
-
text "\
|
25
|
+
text "\nCopy Options:"
|
26
|
+
opt :all, "Copy all folders recursively", :short => :none
|
27
|
+
opt :all_subscribed, "Copy all subscribed folders recursively", :short => :none
|
28
|
+
opt :exclude, "List of mailbox names/patterns that shouldn't be copied", :short => :none, :type => :strings, :multi => true
|
29
|
+
opt :exclude_file, "Filename containing mailbox names/patterns that shouldn't be copied", :short => :none, :type => :string
|
30
|
+
opt :from_folder, "Source folder to copy from", :short => :none, :default => 'INBOX'
|
31
|
+
opt :from_pass, "Source server password (default: prompt)", :short => :none, :type => :string
|
32
|
+
opt :from_user, "Source server username (default: prompt)", :short => :none, :type => :string
|
33
|
+
opt :to_folder, "Destination folder to copy to", :short => :none, :default => 'INBOX'
|
34
|
+
opt :to_pass, "Destination server password (default: prompt)", :short => :none, :type => :string
|
35
|
+
opt :to_user, "Destination server username (default: prompt)", :short => :none, :type => :string
|
26
36
|
|
27
|
-
|
37
|
+
text "\nGeneral Options:"
|
38
|
+
opt :dry_run, "Don't actually make any changes.", :short => '-n'
|
28
39
|
opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
|
29
|
-
opt :from_folder, "Source folder to copy from", :short => :none, :default => 'INBOX'
|
30
|
-
opt :from_pass, "Source server password (Default: prompt)", :short => :none, :type => :string
|
31
|
-
opt :from_user, "Source server username (Default: prompt)", :short => :none, :type => :string
|
32
40
|
opt :max_retries, "Maximum number of times to retry after a recoverable error", :short => :none, :default => 3
|
33
41
|
opt :no_create_folder, "Don't create destination folders that don't already exist", :short => :none
|
34
|
-
opt :
|
35
|
-
opt :
|
36
|
-
opt :to_user, "Destination server username (Default: prompt)", :short => :none, :type => :string
|
42
|
+
opt :ssl_certs, "Path to a trusted certificate bundle to use to verify server SSL certificates", :short => :none, :type => :string
|
43
|
+
opt :ssl_verify, "Verify server SSL certificates", :short => :none
|
37
44
|
opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal", :short => '-V', :default => 'info'
|
38
45
|
end
|
39
46
|
|
@@ -48,44 +55,94 @@ EOS
|
|
48
55
|
Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}"
|
49
56
|
end
|
50
57
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# Prompt for usernames and passwords if necessary.
|
58
|
-
unless options[:from_user]
|
59
|
-
options[:from_user] = ask("Source username (#{options[:from].host}): ")
|
58
|
+
if options[:exclude_file_given]
|
59
|
+
filename = options[:exclude_file]
|
60
|
+
|
61
|
+
Trollop.die :exclude_file, ": file not found: #{filename}" unless File.file?(filename)
|
62
|
+
Trollop.die :exclude_file, ": file cannot be read: #{filename}" unless File.readable?(filename)
|
60
63
|
end
|
61
64
|
|
62
|
-
|
63
|
-
|
65
|
+
# Prevent conflicting options from being used.
|
66
|
+
if options[:all_given]
|
67
|
+
[:all_subscribed, :from_folder, :to_folder].each do |o|
|
68
|
+
Trollop.die :all, "may not be used with --#{o.to_s.gsub('_', '-')}" if options["#{o}_given".to_sym]
|
69
|
+
end
|
64
70
|
end
|
65
71
|
|
66
|
-
|
67
|
-
|
72
|
+
if options[:all_subscribed_given]
|
73
|
+
[:all, :from_folder, :to_folder].each do |o|
|
74
|
+
Trollop.die :all_subscribed, "may not be used with --#{o.to_s.gsub('_', '-')}" if options["#{o}_given".to_sym]
|
75
|
+
end
|
68
76
|
end
|
69
77
|
|
70
|
-
|
71
|
-
|
78
|
+
# Create URIs.
|
79
|
+
uri_from = URI(options[:from])
|
80
|
+
uri_to = URI(options[:to])
|
81
|
+
|
82
|
+
# --all and --all-subscribed options override folders specified in URIs
|
83
|
+
if options[:all_given] || options[:all_subscribed_given]
|
84
|
+
uri_from.path = ''
|
85
|
+
uri_to.path = ''
|
72
86
|
end
|
73
87
|
|
88
|
+
# --from-folder and --to-folder options override folders specified in URIs
|
89
|
+
if options[:from_folder_given] || options[:to_folder_given]
|
90
|
+
uri_from.path = '/' + CGI.escape(options[:from_folder].gsub(/^\//, ''))
|
91
|
+
uri_to.path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
|
92
|
+
end
|
93
|
+
|
94
|
+
# Usernames and passwords specified as arguments override those in the URIs
|
95
|
+
uri_from.user = CGI.escape(options[:from_user]) if options[:from_user]
|
96
|
+
uri_from.password = CGI.escape(options[:from_pass]) if options[:from_pass]
|
97
|
+
uri_to.user = CGI.escape(options[:to_user]) if options[:to_user]
|
98
|
+
uri_to.password = CGI.escape(options[:to_pass]) if options[:to_pass]
|
99
|
+
|
100
|
+
# If usernames/passwords aren't specified in either URIs or args, then prompt.
|
101
|
+
uri_from.user ||= CGI.escape(ask("Source username (#{uri_from.host}): "))
|
102
|
+
uri_from.password ||= CGI.escape(ask("Source password (#{uri_from.host}): ") {|q| q.echo = false })
|
103
|
+
uri_to.user ||= CGI.escape(ask("Destination username (#{uri_to.host}): "))
|
104
|
+
uri_to.password ||= CGI.escape(ask("Destination password (#{uri_to.host}): ") {|q| q.echo = false })
|
105
|
+
|
74
106
|
# Go go go!
|
75
|
-
init(
|
107
|
+
init(
|
108
|
+
options[:verbosity],
|
109
|
+
options[:exclude] ? options[:exclude].flatten : [],
|
110
|
+
options[:exclude_file]
|
111
|
+
)
|
76
112
|
|
77
|
-
|
78
|
-
:fast_scan => options[:fast_scan],
|
79
|
-
:max_retries => options[:max_retries])
|
113
|
+
Net::IMAP.debug = true if @log.level == :insane
|
80
114
|
|
81
|
-
|
82
|
-
:
|
115
|
+
imap_from = Larch::IMAP.new(uri_from,
|
116
|
+
:dry_run => options[:dry_run],
|
117
|
+
:fast_scan => options[:fast_scan],
|
118
|
+
:max_retries => options[:max_retries],
|
119
|
+
:ssl_certs => options[:ssl_certs] || nil,
|
120
|
+
:ssl_verify => options[:ssl_verify]
|
121
|
+
)
|
122
|
+
|
123
|
+
imap_to = Larch::IMAP.new(uri_to,
|
124
|
+
:create_mailbox => !options[:no_create_folder] && !options[:dry_run],
|
125
|
+
:dry_run => options[:dry_run],
|
83
126
|
:fast_scan => options[:fast_scan],
|
84
|
-
:max_retries => options[:max_retries]
|
85
|
-
|
86
|
-
|
87
|
-
|
127
|
+
:max_retries => options[:max_retries],
|
128
|
+
:ssl_certs => options[:ssl_certs] || nil,
|
129
|
+
:ssl_verify => options[:ssl_verify]
|
130
|
+
)
|
131
|
+
|
132
|
+
unless RUBY_PLATFORM =~ /mswin|mingw|bccwin|wince|java/
|
133
|
+
begin
|
134
|
+
for sig in [:SIGINT, :SIGQUIT, :SIGTERM]
|
135
|
+
trap(sig) { @log.fatal "Interrupted (#{sig})"; Kernel.exit }
|
136
|
+
end
|
137
|
+
rescue => e
|
138
|
+
end
|
88
139
|
end
|
89
140
|
|
90
|
-
|
141
|
+
if options[:all_given]
|
142
|
+
copy_all(imap_from, imap_to)
|
143
|
+
elsif options[:all_subscribed_given]
|
144
|
+
copy_all(imap_from, imap_to, true)
|
145
|
+
else
|
146
|
+
copy_folder(imap_from, imap_to)
|
147
|
+
end
|
91
148
|
end
|
data/lib/larch/errors.rb
CHANGED
@@ -0,0 +1,279 @@
|
|
1
|
+
module Larch; class IMAP
|
2
|
+
|
3
|
+
# Represents an IMAP mailbox.
|
4
|
+
class Mailbox
|
5
|
+
attr_reader :attr, :delim, :imap, :name, :state
|
6
|
+
|
7
|
+
# Regex to capture a Message-Id header.
|
8
|
+
REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
|
9
|
+
|
10
|
+
# Minimum time (in seconds) allowed between mailbox scans.
|
11
|
+
SCAN_INTERVAL = 60
|
12
|
+
|
13
|
+
def initialize(imap, name, delim, subscribed, *attr)
|
14
|
+
raise ArgumentError, "must provide a Larch::IMAP instance" unless imap.is_a?(Larch::IMAP)
|
15
|
+
|
16
|
+
@imap = imap
|
17
|
+
@name = name
|
18
|
+
@delim = delim
|
19
|
+
@subscribed = subscribed
|
20
|
+
@attr = attr.flatten
|
21
|
+
|
22
|
+
@ids = {}
|
23
|
+
@last_id = 0
|
24
|
+
@last_scan = nil
|
25
|
+
@mutex = Mutex.new
|
26
|
+
|
27
|
+
# Valid mailbox states are :closed (no mailbox open), :examined (mailbox
|
28
|
+
# open and read-only), or :selected (mailbox open and read-write).
|
29
|
+
@state = :closed
|
30
|
+
|
31
|
+
# Create private convenience methods (debug, info, warn, etc.) to make
|
32
|
+
# logging easier.
|
33
|
+
Logger::LEVELS.each_key do |level|
|
34
|
+
Mailbox.class_eval do
|
35
|
+
define_method(level) do |msg|
|
36
|
+
Larch.log.log(level, "#{@imap.username}@#{@imap.host}: #{@name}: #{msg}")
|
37
|
+
end
|
38
|
+
|
39
|
+
private level
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Appends the specified Larch::IMAP::Message to this mailbox if it doesn't
|
45
|
+
# already exist. Returns +true+ if the message was appended successfully,
|
46
|
+
# +false+ if the message already exists in the mailbox.
|
47
|
+
def append(message)
|
48
|
+
raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Larch::IMAP::Message)
|
49
|
+
return false if has_message?(message)
|
50
|
+
|
51
|
+
@imap.safely do
|
52
|
+
unless imap_select(!!@imap.options[:create_mailbox])
|
53
|
+
raise Larch::IMAP::Error, "mailbox cannot contain messages: #{@name}"
|
54
|
+
end
|
55
|
+
|
56
|
+
debug "appending message: #{message.id}"
|
57
|
+
@imap.conn.append(@name, message.rfc822, message.flags, message.internaldate) unless @imap.options[:dry_run]
|
58
|
+
end
|
59
|
+
|
60
|
+
true
|
61
|
+
end
|
62
|
+
alias << append
|
63
|
+
|
64
|
+
# Iterates through Larch message ids in this mailbox, yielding each one to the
|
65
|
+
# provided block.
|
66
|
+
def each
|
67
|
+
scan
|
68
|
+
@ids.dup.each_key {|id| yield id }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Gets a Net::IMAP::Envelope for the specified message id.
|
72
|
+
def envelope(message_id)
|
73
|
+
scan
|
74
|
+
raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
|
75
|
+
|
76
|
+
debug "fetching envelope: #{message_id}"
|
77
|
+
imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
78
|
+
end
|
79
|
+
|
80
|
+
# Fetches a Larch::IMAP::Message struct representing the message with the
|
81
|
+
# specified Larch message id.
|
82
|
+
def fetch(message_id, peek = false)
|
83
|
+
scan
|
84
|
+
raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
|
85
|
+
|
86
|
+
debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
|
87
|
+
data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
|
88
|
+
|
89
|
+
Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
|
90
|
+
data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
|
91
|
+
end
|
92
|
+
alias [] fetch
|
93
|
+
|
94
|
+
# Returns +true+ if a message with the specified Larch <em>message_id</em>
|
95
|
+
# exists in this mailbox, +false+ otherwise.
|
96
|
+
def has_message?(message_id)
|
97
|
+
scan
|
98
|
+
@ids.has_key?(message_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Gets the number of messages in this mailbox.
|
102
|
+
def length
|
103
|
+
scan
|
104
|
+
@ids.length
|
105
|
+
end
|
106
|
+
alias size length
|
107
|
+
|
108
|
+
# Same as fetch, but doesn't mark the message as seen.
|
109
|
+
def peek(message_id)
|
110
|
+
fetch(message_id, true)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Resets the mailbox state.
|
114
|
+
def reset
|
115
|
+
@mutex.synchronize { @state = :closed }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Fetches message headers from this mailbox.
|
119
|
+
def scan
|
120
|
+
return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
|
121
|
+
|
122
|
+
begin
|
123
|
+
return unless imap_examine
|
124
|
+
rescue Error => e
|
125
|
+
return if @imap.options[:create_mailbox]
|
126
|
+
raise
|
127
|
+
end
|
128
|
+
|
129
|
+
last_id = @imap.safely { @imap.conn.responses['EXISTS'].last }
|
130
|
+
@mutex.synchronize { @last_scan = Time.now }
|
131
|
+
return if last_id == @last_id
|
132
|
+
|
133
|
+
range = (@last_id + 1)..last_id
|
134
|
+
@mutex.synchronize { @last_id = last_id }
|
135
|
+
|
136
|
+
info "fetching message headers #{range}" <<
|
137
|
+
(@imap.options[:fast_scan] ? ' (fast scan)' : '')
|
138
|
+
|
139
|
+
fields = if @imap.options[:fast_scan]
|
140
|
+
['UID', 'RFC822.SIZE', 'INTERNALDATE']
|
141
|
+
else
|
142
|
+
"(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
|
143
|
+
end
|
144
|
+
|
145
|
+
imap_fetch(range.begin..-1, fields).each do |data|
|
146
|
+
id = create_id(data)
|
147
|
+
|
148
|
+
unless uid = data.attr['UID']
|
149
|
+
error "UID not in IMAP response for message: #{id}"
|
150
|
+
next
|
151
|
+
end
|
152
|
+
|
153
|
+
if Larch.log.level == :debug && @ids.has_key?(id)
|
154
|
+
envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
155
|
+
debug "duplicate message? #{id} (Subject: #{envelope.subject})"
|
156
|
+
end
|
157
|
+
|
158
|
+
@mutex.synchronize { @ids[id] = uid }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Subscribes to this mailbox.
|
163
|
+
def subscribe(force = false)
|
164
|
+
return if subscribed? && !force
|
165
|
+
@imap.safely { @imap.conn.subscribe(@name) } unless @imap.options[:dry_run]
|
166
|
+
@mutex.synchronize { @subscribed = true }
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns +true+ if this mailbox is subscribed, +false+ otherwise.
|
170
|
+
def subscribed?
|
171
|
+
@subscribed
|
172
|
+
end
|
173
|
+
|
174
|
+
# Unsubscribes from this mailbox.
|
175
|
+
def unsubscribe(force = false)
|
176
|
+
return unless subscribed? || force
|
177
|
+
@imap.safely { @imap.conn.unsubscribe(@name) } unless @imap.options[:dry_run]
|
178
|
+
@mutex.synchronize { @subscribed = false }
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
# Creates an id suitable for uniquely identifying a specific message across
|
184
|
+
# servers (we hope).
|
185
|
+
#
|
186
|
+
# If the given message data includes a valid Message-Id header, then that will
|
187
|
+
# be used to generate an MD5 hash. Otherwise, the hash will be generated based
|
188
|
+
# on the message's RFC822.SIZE and INTERNALDATE.
|
189
|
+
def create_id(data)
|
190
|
+
['RFC822.SIZE', 'INTERNALDATE'].each do |a|
|
191
|
+
raise Error, "required data not in IMAP response: #{a}" unless data.attr[a]
|
192
|
+
end
|
193
|
+
|
194
|
+
if data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'] =~ REGEX_MESSAGE_ID
|
195
|
+
Digest::MD5.hexdigest($1)
|
196
|
+
else
|
197
|
+
Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
|
198
|
+
Time.parse(data.attr['INTERNALDATE']).to_i))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Examines the mailbox. If _force_ is true, the mailbox will be examined even
|
203
|
+
# if it is already selected (which isn't necessary unless you want to ensure
|
204
|
+
# that it's in a read-only state).
|
205
|
+
def imap_examine(force = false)
|
206
|
+
return false if @attr.include?(:Noselect)
|
207
|
+
return true if @state == :examined || (!force && @state == :selected)
|
208
|
+
|
209
|
+
@imap.safely do
|
210
|
+
begin
|
211
|
+
@mutex.synchronize { @state = :closed }
|
212
|
+
|
213
|
+
debug "examining mailbox"
|
214
|
+
@imap.conn.examine(@name)
|
215
|
+
|
216
|
+
@mutex.synchronize { @state = :examined }
|
217
|
+
|
218
|
+
rescue Net::IMAP::NoResponseError => e
|
219
|
+
raise Error, "unable to examine mailbox: #{e.message}"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
return true
|
224
|
+
end
|
225
|
+
|
226
|
+
# Fetches the specified _fields_ for the specified _set_ of message sequence
|
227
|
+
# ids (either a Range or an Array of ids).
|
228
|
+
def imap_fetch(set, fields)
|
229
|
+
@imap.safely do
|
230
|
+
imap_examine
|
231
|
+
@imap.conn.fetch(set, fields)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Selects the mailbox if it is not already selected. If the mailbox does not
|
236
|
+
# exist and _create_ is +true+, it will be created. Otherwise, a
|
237
|
+
# Larch::IMAP::Error will be raised.
|
238
|
+
def imap_select(create = false)
|
239
|
+
return false if @attr.include?(:Noselect)
|
240
|
+
return true if @state == :selected
|
241
|
+
|
242
|
+
@imap.safely do
|
243
|
+
begin
|
244
|
+
@mutex.synchronize { @state = :closed }
|
245
|
+
|
246
|
+
debug "selecting mailbox"
|
247
|
+
@imap.conn.select(@name)
|
248
|
+
|
249
|
+
@mutex.synchronize { @state = :selected }
|
250
|
+
|
251
|
+
rescue Net::IMAP::NoResponseError => e
|
252
|
+
raise Error, "unable to select mailbox: #{e.message}" unless create
|
253
|
+
|
254
|
+
info "creating mailbox: #{@name}"
|
255
|
+
|
256
|
+
begin
|
257
|
+
@imap.conn.create(@name) unless @imap.options[:dry_run]
|
258
|
+
retry
|
259
|
+
rescue => e
|
260
|
+
raise Error, "unable to create mailbox: #{e.message}"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
return true
|
266
|
+
end
|
267
|
+
|
268
|
+
# Fetches the specified _fields_ for the specified _set_ of UIDs (either a
|
269
|
+
# Range or an Array of UIDs).
|
270
|
+
def imap_uid_fetch(set, fields)
|
271
|
+
@imap.safely do
|
272
|
+
imap_examine
|
273
|
+
@imap.conn.uid_fetch(set, fields)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
|
279
|
+
end; end
|