larch 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/larch.rb CHANGED
@@ -1,123 +1,192 @@
1
- # Append this file's directory to the include path if it's not there already.
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
- def init(log_level = :info)
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
- @copied = 0
27
- @failed = 0
28
- @total = 0
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
- # Copies messages from _source_ to _dest_ if they don't already exist in
32
- # _dest_. Both _source_ and _dest_ must be instances of Larch::IMAP.
33
- def copy(source, dest)
34
- raise ArgumentError, "source must be a Larch::IMAP instance" unless source.is_a?(IMAP)
35
- raise ArgumentError, "dest must be a Larch::IMAP instance" unless dest.is_a?(IMAP)
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
- msgq = SizedQueue.new(8)
38
- mutex = Mutex.new
51
+ @copied = 0
52
+ @failed = 0
53
+ @total = 0
39
54
 
40
- @copied = 0
41
- @failed = 0
42
- @total = 0
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
- @log.info "copying messages from #{source.uri} to #{dest.uri}"
59
+ mailbox_to = imap_to.mailbox(mailbox_from.name, mailbox_from.delim)
60
+ mailbox_to.subscribe if mailbox_from.subscribed?
45
61
 
46
- source_scan = Thread.new do
47
- source.scan_mailbox
62
+ copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
48
63
  end
49
64
 
50
- dest_scan = Thread.new do
51
- dest.scan_mailbox
52
- end
65
+ rescue => e
66
+ @log.fatal e.message
53
67
 
54
- source_scan.join
55
- dest_scan.join
68
+ ensure
69
+ summary
70
+ end
56
71
 
57
- source_copy = Thread.new do
58
- begin
59
- mutex.synchronize { @total = source.length }
60
-
61
- source.each do |id|
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
- rescue => e
75
- @log.fatal e.message
78
+ @copied = 0
79
+ @failed = 0
80
+ @total = 0
76
81
 
77
- ensure
78
- msgq << :finished
79
- end
80
- end
82
+ from_name = imap_from.uri_mailbox || 'INBOX'
83
+ to_name = imap_to.uri_mailbox || 'INBOX'
81
84
 
82
- dest_copy = Thread.new do
83
- begin
84
- while msg = msgq.pop do
85
- break if msg == :finished
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
- if msg.envelope.from
88
- env_from = msg.envelope.from.first
89
- from = "#{env_from.mailbox}@#{env_from.host}"
90
- else
91
- from = '?'
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
- @log.info "copying message: #{from} - #{msg.envelope.subject}"
95
- dest << msg
112
+ return if excluded?(mailbox_from.name) || excluded?(mailbox_to.name)
96
113
 
97
- mutex.synchronize { @copied += 1 }
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
- rescue IMAP::FatalError => e
101
- @log.fatal e.message
134
+ @log.info "copying message: #{from} - #{msg.envelope.subject}"
102
135
 
103
- rescue => e
104
- mutex.synchronize { @failed += 1 }
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
- retry
143
+ next
107
144
  end
108
145
  end
146
+ end
109
147
 
110
- dest_copy.join
148
+ def excluded?(name)
149
+ name = name.downcase
111
150
 
112
- source.disconnect
113
- dest.disconnect
151
+ @exclude.each do |e|
152
+ return true if (e.is_a?(Regexp) ? !!(name =~ e) : File.fnmatch?(e, name))
153
+ end
114
154
 
115
- summary
155
+ return false
116
156
  end
117
157
 
118
- def summary
119
- @log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
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.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-03-16 00:00:00 -07:00
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.1
78
+ rubygems_version: 1.3.2
75
79
  signing_key:
76
- specification_version: 2
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