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/lib/larch.rb
CHANGED
@@ -1,123 +1,192 @@
|
|
1
|
-
#
|
1
|
+
# Prepend this file's directory to the include path if it's not there already.
|
2
2
|
$:.unshift(File.dirname(File.expand_path(__FILE__)))
|
3
3
|
$:.uniq!
|
4
4
|
|
5
5
|
require 'cgi'
|
6
6
|
require 'digest/md5'
|
7
7
|
require 'net/imap'
|
8
|
-
require 'thread'
|
9
8
|
require 'time'
|
10
9
|
require 'uri'
|
11
10
|
|
12
|
-
require 'larch/util'
|
13
11
|
require 'larch/errors'
|
14
12
|
require 'larch/imap'
|
13
|
+
require 'larch/imap/mailbox'
|
15
14
|
require 'larch/logger'
|
16
15
|
require 'larch/version'
|
17
16
|
|
18
17
|
module Larch
|
19
18
|
|
20
19
|
class << self
|
21
|
-
attr_reader :log
|
20
|
+
attr_reader :log, :exclude
|
22
21
|
|
23
|
-
|
22
|
+
EXCLUDE_COMMENT = /#.*$/
|
23
|
+
EXCLUDE_REGEX = /^\s*\/(.*)\/\s*/
|
24
|
+
GLOB_PATTERNS = {'*' => '.*', '?' => '.'}
|
25
|
+
|
26
|
+
def init(log_level = :info, exclude = [], exclude_file = nil)
|
24
27
|
@log = Logger.new(log_level)
|
25
28
|
|
26
|
-
@
|
27
|
-
|
28
|
-
|
29
|
+
@exclude = exclude.map do |e|
|
30
|
+
if e =~ EXCLUDE_REGEX
|
31
|
+
Regexp.new($1, Regexp::IGNORECASE)
|
32
|
+
else
|
33
|
+
glob_to_regex(e.strip)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
load_exclude_file(exclude_file) if exclude_file
|
38
|
+
|
39
|
+
# Stats
|
40
|
+
@copied = 0
|
41
|
+
@failed = 0
|
42
|
+
@total = 0
|
29
43
|
end
|
30
44
|
|
31
|
-
#
|
32
|
-
#
|
33
|
-
def
|
34
|
-
raise ArgumentError, "
|
35
|
-
raise ArgumentError, "
|
45
|
+
# Recursively copies all messages in all folders from the source to the
|
46
|
+
# destination.
|
47
|
+
def copy_all(imap_from, imap_to, subscribed_only = false)
|
48
|
+
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
49
|
+
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
36
50
|
|
37
|
-
|
38
|
-
|
51
|
+
@copied = 0
|
52
|
+
@failed = 0
|
53
|
+
@total = 0
|
39
54
|
|
40
|
-
|
41
|
-
|
42
|
-
|
55
|
+
imap_from.each_mailbox do |mailbox_from|
|
56
|
+
next if excluded?(mailbox_from.name)
|
57
|
+
next if subscribed_only && !mailbox_from.subscribed?
|
43
58
|
|
44
|
-
|
59
|
+
mailbox_to = imap_to.mailbox(mailbox_from.name, mailbox_from.delim)
|
60
|
+
mailbox_to.subscribe if mailbox_from.subscribed?
|
45
61
|
|
46
|
-
|
47
|
-
source.scan_mailbox
|
62
|
+
copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
|
48
63
|
end
|
49
64
|
|
50
|
-
|
51
|
-
|
52
|
-
end
|
65
|
+
rescue => e
|
66
|
+
@log.fatal e.message
|
53
67
|
|
54
|
-
|
55
|
-
|
68
|
+
ensure
|
69
|
+
summary
|
70
|
+
end
|
56
71
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
next if dest.has_message?(id)
|
63
|
-
|
64
|
-
begin
|
65
|
-
msgq << source.peek(id)
|
66
|
-
rescue Larch::IMAP::Error => e
|
67
|
-
# TODO: Keep failed message envelopes in a buffer for later output?
|
68
|
-
mutex.synchronize { @failed += 1 }
|
69
|
-
@log.error e.message
|
70
|
-
next
|
71
|
-
end
|
72
|
-
end
|
72
|
+
# Copies the messages in a single IMAP folder (non-recursively) from the
|
73
|
+
# source to the destination.
|
74
|
+
def copy_folder(imap_from, imap_to)
|
75
|
+
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
76
|
+
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
73
77
|
|
74
|
-
|
75
|
-
|
78
|
+
@copied = 0
|
79
|
+
@failed = 0
|
80
|
+
@total = 0
|
76
81
|
|
77
|
-
|
78
|
-
|
79
|
-
end
|
80
|
-
end
|
82
|
+
from_name = imap_from.uri_mailbox || 'INBOX'
|
83
|
+
to_name = imap_to.uri_mailbox || 'INBOX'
|
81
84
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
85
|
+
return if excluded?(from_name) || excluded?(to_name)
|
86
|
+
|
87
|
+
copy_messages(imap_from, imap_from.mailbox(from_name), imap_to,
|
88
|
+
imap_to.mailbox(to_name))
|
89
|
+
|
90
|
+
imap_from.disconnect
|
91
|
+
imap_to.disconnect
|
92
|
+
|
93
|
+
rescue => e
|
94
|
+
@log.fatal e.message
|
95
|
+
|
96
|
+
ensure
|
97
|
+
summary
|
98
|
+
end
|
99
|
+
|
100
|
+
def summary
|
101
|
+
@log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
86
105
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
106
|
+
def copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
|
107
|
+
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
108
|
+
raise ArgumentError, "mailbox_from must be a Larch::IMAP::Mailbox instance" unless mailbox_from.is_a?(IMAP::Mailbox)
|
109
|
+
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
110
|
+
raise ArgumentError, "mailbox_to must be a Larch::IMAP::Mailbox instance" unless mailbox_to.is_a?(IMAP::Mailbox)
|
93
111
|
|
94
|
-
|
95
|
-
dest << msg
|
112
|
+
return if excluded?(mailbox_from.name) || excluded?(mailbox_to.name)
|
96
113
|
|
97
|
-
|
114
|
+
@log.info "copying messages from #{imap_from.host}/#{mailbox_from.name} to #{imap_to.host}/#{mailbox_to.name}"
|
115
|
+
|
116
|
+
imap_from.connect
|
117
|
+
imap_to.connect
|
118
|
+
|
119
|
+
@total += mailbox_from.length
|
120
|
+
|
121
|
+
mailbox_from.each do |id|
|
122
|
+
next if mailbox_to.has_message?(id)
|
123
|
+
|
124
|
+
begin
|
125
|
+
msg = mailbox_from.peek(id)
|
126
|
+
|
127
|
+
if msg.envelope.from
|
128
|
+
env_from = msg.envelope.from.first
|
129
|
+
from = "#{env_from.mailbox}@#{env_from.host}"
|
130
|
+
else
|
131
|
+
from = '?'
|
98
132
|
end
|
99
133
|
|
100
|
-
|
101
|
-
@log.fatal e.message
|
134
|
+
@log.info "copying message: #{from} - #{msg.envelope.subject}"
|
102
135
|
|
103
|
-
|
104
|
-
|
136
|
+
mailbox_to << msg
|
137
|
+
@copied += 1
|
138
|
+
|
139
|
+
rescue Larch::IMAP::Error => e
|
140
|
+
# TODO: Keep failed message envelopes in a buffer for later output?
|
141
|
+
@failed += 1
|
105
142
|
@log.error e.message
|
106
|
-
|
143
|
+
next
|
107
144
|
end
|
108
145
|
end
|
146
|
+
end
|
109
147
|
|
110
|
-
|
148
|
+
def excluded?(name)
|
149
|
+
name = name.downcase
|
111
150
|
|
112
|
-
|
113
|
-
|
151
|
+
@exclude.each do |e|
|
152
|
+
return true if (e.is_a?(Regexp) ? !!(name =~ e) : File.fnmatch?(e, name))
|
153
|
+
end
|
114
154
|
|
115
|
-
|
155
|
+
return false
|
116
156
|
end
|
117
157
|
|
118
|
-
def
|
119
|
-
|
158
|
+
def glob_to_regex(str)
|
159
|
+
str.gsub!(/(.)/) {|c| GLOB_PATTERNS[$1] || Regexp.escape(c) }
|
160
|
+
Regexp.new("^#{str}$", Regexp::IGNORECASE)
|
120
161
|
end
|
162
|
+
|
163
|
+
def load_exclude_file(filename)
|
164
|
+
@exclude ||= []
|
165
|
+
lineno = 0
|
166
|
+
|
167
|
+
File.open(filename, 'rb') do |f|
|
168
|
+
f.each do |line|
|
169
|
+
lineno += 1
|
170
|
+
|
171
|
+
# Strip comments.
|
172
|
+
line.sub!(EXCLUDE_COMMENT, '')
|
173
|
+
line.strip!
|
174
|
+
|
175
|
+
# Skip empty lines.
|
176
|
+
next if line.empty?
|
177
|
+
|
178
|
+
if line =~ EXCLUDE_REGEX
|
179
|
+
@exclude << Regexp.new($1, Regexp::IGNORECASE)
|
180
|
+
else
|
181
|
+
@exclude << glob_to_regex(line)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
rescue => e
|
187
|
+
raise Larch::IMAP::FatalError, "error in exclude file at line #{lineno}: #{e}"
|
188
|
+
end
|
189
|
+
|
121
190
|
end
|
122
191
|
|
123
|
-
end
|
192
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: larch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
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-05-10 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -41,16 +41,20 @@ extensions: []
|
|
41
41
|
extra_rdoc_files: []
|
42
42
|
|
43
43
|
files:
|
44
|
+
- HISTORY
|
44
45
|
- LICENSE
|
46
|
+
- README.rdoc
|
45
47
|
- bin/larch
|
46
48
|
- lib/larch.rb
|
47
49
|
- lib/larch/errors.rb
|
48
50
|
- lib/larch/imap.rb
|
51
|
+
- lib/larch/imap/mailbox.rb
|
49
52
|
- lib/larch/logger.rb
|
50
|
-
- lib/larch/util.rb
|
51
53
|
- lib/larch/version.rb
|
52
54
|
has_rdoc: false
|
53
55
|
homepage: http://github.com/rgrove/larch/
|
56
|
+
licenses: []
|
57
|
+
|
54
58
|
post_install_message:
|
55
59
|
rdoc_options: []
|
56
60
|
|
@@ -71,9 +75,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
75
|
requirements: []
|
72
76
|
|
73
77
|
rubyforge_project:
|
74
|
-
rubygems_version: 1.3.
|
78
|
+
rubygems_version: 1.3.2
|
75
79
|
signing_key:
|
76
|
-
specification_version:
|
80
|
+
specification_version: 3
|
77
81
|
summary: Larch syncs messages from one IMAP server to another. Awesomely.
|
78
82
|
test_files: []
|
79
83
|
|
data/lib/larch/util.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
class Module
|
2
|
-
|
3
|
-
# Java-style whole-method synchronization, shamelessly stolen from Sup:
|
4
|
-
# http://sup.rubyforge.org. Assumes the existence of a <tt>@mutex</tt>
|
5
|
-
# variable.
|
6
|
-
def synchronized(*methods)
|
7
|
-
methods.each do |method|
|
8
|
-
class_eval <<-EOF
|
9
|
-
alias unsync_#{method} #{method}
|
10
|
-
def #{method}(*a, &b)
|
11
|
-
@mutex.synchronize { unsync_#{method}(*a, &b) }
|
12
|
-
end
|
13
|
-
EOF
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|