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/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