larch 1.0.2 → 1.1.0dev20091006

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY CHANGED
@@ -1,6 +1,24 @@
1
1
  Larch History
2
2
  ================================================================================
3
3
 
4
+ Version 1.1.0 (git)
5
+ * Mailbox and message state information is now stored in a local SQLite
6
+ database, which allows Larch to resync and resume interrupted syncs much
7
+ more quickly without having to rescan all messages. As a result, SQLite 3 is
8
+ now a dependency.
9
+ * Larch now loads config options from ~/.larch/config.yaml if it exists, or
10
+ from the file specified by the --config command-line option. This file may
11
+ contain multiple sections. If a section name is specified via the
12
+ command-line, Larch will use the options in that section for the session;
13
+ otherwise it will use the options in the "default" section. See the README
14
+ for more details.
15
+ * Folders are now copied recursively by default.
16
+ * Progress information is now displayed regularly while scanning large
17
+ mailboxes.
18
+ * Added short versions of common command-line options.
19
+ * The --fast-scan option has been removed.
20
+ * Fixed encoding issues when creating mailboxes and getting mailbox lists.
21
+
4
22
  Version 1.0.2 (2009-08-05)
5
23
  * Fixed a bug that caused Larch to try to set the read-only \Recent flag on
6
24
  the destination server.
data/README.rdoc CHANGED
@@ -2,90 +2,104 @@
2
2
 
3
3
  Larch is a tool to copy messages from one IMAP server to another quickly and
4
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.
5
+ destination and robust enough to deal with interruptions caused by flaky
6
+ connections or misbehaving servers.
6
7
 
7
8
  Larch is particularly well-suited for copying email to, from, or between Gmail
8
9
  accounts.
9
10
 
10
11
  *Author*:: Ryan Grove (mailto:ryan@wonko.com)
11
- *Version*:: 1.0.2 (2009-08-05)
12
+ *Version*:: 1.1.0 (git)
12
13
  *Copyright*:: Copyright (c) 2009 Ryan Grove. All rights reserved.
13
14
  *License*:: GPL 2.0 (http://opensource.org/licenses/gpl-2.0.php)
14
15
  *Website*:: http://github.com/rgrove/larch
15
16
 
16
17
  == Installation
17
18
 
18
- Install Larch via RubyGems:
19
+ Install the latest stable version of Larch via RubyGems:
19
20
 
20
21
  gem install larch
21
22
 
22
- == Usage
23
+ Or you can install the latest development snapshot. To do so, first install
24
+ Gemcutter (if you haven't already):
23
25
 
24
- larch --from <uri> --to <uri> [options]
26
+ gem install gemcutter
27
+ gem tumble
25
28
 
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)
29
+ Then install the latest Larch prerelease gem:
43
30
 
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:
31
+ gem install larch --prerelease
66
32
 
67
- larch --from <uri> --to <uri> [options]
33
+ == Usage
68
34
 
69
- For an overview of all available command-line options, run:
35
+ larch [config section] [options]
36
+ larch --from <uri> --to <uri> [options]
70
37
 
71
- larch --help
38
+ Server Options:
39
+ --from, -f <s>: URI of the source IMAP server.
40
+ --from-folder, -F <s>: Source folder to copy from (default: INBOX)
41
+ --from-pass, -p <s>: Source server password (default: prompt)
42
+ --from-user, -u <s>: Source server username (default: prompt)
43
+ --to, -t <s>: URI of the destination IMAP server.
44
+ --to-folder, -T <s>: Destination folder to copy to (default: INBOX)
45
+ --to-pass, -P <s>: Destination server password (default: prompt)
46
+ --to-user, -U <s>: Destination server username (default: prompt)
47
+
48
+ Sync Options:
49
+ --all, -a: Copy all folders recursively
50
+ --all-subscribed, -s: Copy all subscribed folders recursively
51
+ --exclude <s+>: List of mailbox names/patterns that shouldn't be
52
+ copied
53
+ --exclude-file <s>: Filename containing mailbox names/patterns that
54
+ shouldn't be copied
72
55
 
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:
56
+ General Options:
57
+ --config, -c <s>: Specify a non-default config file to use (default:
58
+ ~/.larch/config.yaml)
59
+ --database <s>: Specify a non-default message database to use
60
+ (default: ~/.larch/larch.db)
61
+ --dry-run, -n: Don't actually make any changes
62
+ --max-retries <i>: Maximum number of times to retry after a recoverable
63
+ error (default: 3)
64
+ --no-create-folder: Don't create destination folders that don't already
65
+ exist
66
+ --ssl-certs <s>: Path to a trusted certificate bundle to use to
67
+ verify server SSL certificates
68
+ --ssl-verify: Verify server SSL certificates
69
+ --verbosity, -V <s>: Output verbosity: debug, info, warn, error, or fatal
70
+ (default: info)
71
+ --version, -v: Print version and exit
72
+ --help, -h: Show this message
73
+
74
+ == Usage Examples
75
+
76
+ Larch is run from the command line. The following examples demonstrate how to
77
+ run Larch using only command line arguments, but you may also place these
78
+ options in a config file and run Larch without any arguments if you prefer. See
79
+ the "Configuration" section below for more details.
80
+
81
+ For an overview of all available options, run:
82
+
83
+ larch -h
84
+
85
+ At a minimum, you must <b>specify a source server and a destination server</b>
86
+ in the form of IMAP URIs:
76
87
 
77
88
  larch --from imap://mail.example.com --to imap://imap.gmail.com
78
89
 
90
+ Larch will prompt you for the necessary usernames and passwords, then sync the
91
+ contents of the source's +INBOX+ folder to the destination's INBOX folder.
92
+
79
93
  To <b>connect using SSL</b>, specify a URI beginning with <tt>imaps://</tt>:
80
94
 
81
95
  larch --from imaps://mail.example.com --to imaps://imap.gmail.com
82
96
 
83
97
  If you'd like to <b>sync a specific folder</b> other than +INBOX+, specify the
84
98
  source and destination folders using <tt>--from-folder</tt> and
85
- <tt>--to-folder</tt>:
99
+ <tt>--to-folder</tt>. Folder names containing spaces must be enclosed in quotes:
86
100
 
87
101
  larch --from imaps://mail.example.com --to imaps://imap.gmail.com \
88
- --from-folder "Sent Mail" --to-folder "Sent Mail"
102
+ --from-folder 'Sent Mail' --to-folder 'Sent Mail'
89
103
 
90
104
  To <b>sync all folders</b>, use the <tt>--all</tt> option (or
91
105
  <tt>--all-subscribed</tt> if you only want to <b>sync subscribed folders</b>):
@@ -99,7 +113,7 @@ already exist. To prevent this, add the <tt>--no-create-folder</tt> option:
99
113
  --no-create-folder
100
114
 
101
115
  You can <b>prevent Larch from syncing one or more folders</b> by using the
102
- <tt>--exclude</tt> option:
116
+ <tt>--exclude</tt> option, which accepts multiple arguments:
103
117
 
104
118
  larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all \
105
119
  --exclude Spam Trash Drafts "[Gmail]/*"
@@ -112,10 +126,83 @@ option:
112
126
  --exclude-file exclude.txt
113
127
 
114
128
  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
129
+ lists. You may also use a regular expression by enclosing a pattern in
116
130
  forward slashes, so the previous example could be achieved with the
117
131
  pattern <tt>/(Spam|Trash|Drafts|\[Gmail\]\/.*)/</tt>
118
132
 
133
+ == Configuration
134
+
135
+ While it's possible to control Larch entirely from the command line, this can be
136
+ inconvenient if you need to specify a lot of options or if you run Larch
137
+ frequently and can't always remember which options to use. Using a configuration
138
+ file can simplify things.
139
+
140
+ By default, Larch looks for a config file at <tt>~/.larch/config.yaml</tt> and
141
+ uses it if found. You may specify a custom config file using the
142
+ <tt>--config</tt> command line option.
143
+
144
+ The Larch configuration file is a simple YAML[http://yaml.org/] file that may
145
+ contain multiple sections, each with a different set of options, as well as a
146
+ special +default+ section. The options in the +default+ section will be used
147
+ unless they're overridden either in another config section or on the command
148
+ line.
149
+
150
+ === Example
151
+
152
+ Here's a sample Larch config file:
153
+
154
+ default:
155
+ all-subscribed: true # Copy all subscribed folders by default
156
+
157
+ # Copy mail from Gmail to my server, excluding stuff I don't want.
158
+ gmail to my server:
159
+ from: imaps://imap.gmail.com
160
+ from-user: example
161
+ from-pass: secret
162
+
163
+ to: imaps://mail.example.com
164
+ to-user: example
165
+ to-pass: secret
166
+
167
+ exclude:
168
+ - "[Gmail]/Sent Mail"
169
+ - "[Gmail]/Spam"
170
+ - "[Gmail]/Trash"
171
+
172
+ # Copy mail from my INBOX to Gmail's INBOX
173
+ my inbox to gmail inbox:
174
+ all-subscribed: false
175
+
176
+ from: imaps://mail.example.com
177
+ from-folder: INBOX
178
+ from-user: example
179
+ from-pass: secret
180
+
181
+ to: imaps://imap.gmail.com
182
+ to-folder: INBOX
183
+ to-user: example
184
+ to-pass: secret
185
+
186
+ This file contains three sections. The options from +default+ will be used in
187
+ all other sections as well unless they're overridden.
188
+
189
+ To specify which config section you want Larch to use, just pass its name on the
190
+ command line (use quotes if the name contains spaces):
191
+
192
+ larch 'gmail to my server'
193
+
194
+ If you specify additional command line options, they'll override options in the
195
+ config file:
196
+
197
+ larch 'gmail to my server' --from-user anotheruser
198
+
199
+ Running Larch with no command line arguments will cause the +default+ section
200
+ to be used. With the example above, this will result in an error since the
201
+ +default+ section doesn't contain the required +from+ and +to+ options, but if
202
+ you only need to use Larch with a single configuration, you could use the
203
+ +default+ section for everything and save yourself some typing on the command
204
+ line.
205
+
119
206
  == Server Compatibility
120
207
 
121
208
  Larch should work well with any server that properly supports
data/bin/larch CHANGED
@@ -11,122 +11,97 @@ module Larch
11
11
  # Parse command-line options.
12
12
  options = Trollop.options do
13
13
  version "Larch #{APP_VERSION}\n" << APP_COPYRIGHT
14
- banner <<EOS
14
+ banner <<-EOS
15
15
  Larch syncs messages from one IMAP server to another. Awesomely.
16
16
 
17
17
  Usage:
18
+ larch [config section] [options]
18
19
  larch --from <uri> --to <uri> [options]
19
20
 
20
- Required:
21
+ Server Options:
21
22
  EOS
22
- opt :from, "URI of the source IMAP server.", :short => '-f', :type => :string, :required => true
23
- opt :to, "URI of the destination IMAP server.", :short => '-t', :type => :string, :required => true
24
-
25
- text "\nCopy Options:"
26
- opt :all, "Copy all folders recursively", :short => :none
27
- opt :all_subscribed, "Copy all subscribed folders recursively", :short => :none
23
+ opt :from, "URI of the source IMAP server.", :short => '-f', :type => :string
24
+ opt :from_folder, "Source folder to copy from", :short => '-F', :default => Config::DEFAULT['from-folder']
25
+ opt :from_pass, "Source server password (default: prompt)", :short => '-p', :type => :string
26
+ opt :from_user, "Source server username (default: prompt)", :short => '-u', :type => :string
27
+ opt :to, "URI of the destination IMAP server.", :short => '-t', :type => :string
28
+ opt :to_folder, "Destination folder to copy to", :short => '-T', :default => Config::DEFAULT['to-folder']
29
+ opt :to_pass, "Destination server password (default: prompt)", :short => '-P', :type => :string
30
+ opt :to_user, "Destination server username (default: prompt)", :short => '-U', :type => :string
31
+
32
+ text "\nSync Options:"
33
+ opt :all, "Copy all folders recursively", :short => '-a'
34
+ opt :all_subscribed, "Copy all subscribed folders recursively", :short => '-s'
28
35
  opt :exclude, "List of mailbox names/patterns that shouldn't be copied", :short => :none, :type => :strings, :multi => true
29
36
  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
36
37
 
37
38
  text "\nGeneral Options:"
38
- opt :dry_run, "Don't actually make any changes.", :short => '-n'
39
- opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
40
- opt :max_retries, "Maximum number of times to retry after a recoverable error", :short => :none, :default => 3
39
+ opt :config, "Specify a non-default config file to use", :short => '-c', :default => Config::DEFAULT['config']
40
+ opt :database, "Specify a non-default message database to use", :short => :none, :default => Config::DEFAULT['database']
41
+ opt :dry_run, "Don't actually make any changes", :short => '-n'
42
+ opt :max_retries, "Maximum number of times to retry after a recoverable error", :short => :none, :default => Config::DEFAULT['max-retries']
41
43
  opt :no_create_folder, "Don't create destination folders that don't already exist", :short => :none
42
44
  opt :ssl_certs, "Path to a trusted certificate bundle to use to verify server SSL certificates", :short => :none, :type => :string
43
45
  opt :ssl_verify, "Verify server SSL certificates", :short => :none
44
- opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal", :short => '-V', :default => 'info'
45
- end
46
-
47
- # Validate command-line options.
48
- [:from, :to].each do |sym|
49
- unless options[sym] =~ IMAP::REGEX_URI
50
- Trollop.die sym, "must be a valid IMAP URI (e.g. imap://example.com)"
51
- end
52
- end
53
-
54
- unless Logger::LEVELS.has_key?(options[:verbosity].to_sym)
55
- Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}"
56
- end
57
-
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)
46
+ opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal", :short => '-V', :default => Config::DEFAULT['verbosity']
63
47
  end
64
48
 
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
49
+ # Load config.
50
+ config = Config.new(ARGV.shift || 'default', options[:config], options)
51
+
52
+ if options[:config_given]
53
+ Trollop.die :config, ": file not found: #{options[:config]}" unless File.exist?(options[:config])
70
54
  end
71
55
 
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
56
+ # Validate config.
57
+ begin
58
+ config.validate
59
+ rescue Config::Error => e
60
+ abort "Config error: #{e}"
76
61
  end
77
62
 
78
63
  # Create URIs.
79
- uri_from = URI(options[:from])
80
- uri_to = URI(options[:to])
64
+ uri_from = URI(config.from)
65
+ uri_to = URI(config.to)
66
+
67
+ # Use --from-folder and --to-folder unless folders were specified in the URIs.
68
+ uri_from.path = uri_from.path.empty? ? '/' + CGI.escape(config.from_folder.gsub(/^\//, '')) : uri_from.path
69
+ uri_to.path = uri_to.path.empty? ? '/' + CGI.escape(config.to_folder.gsub(/^\//, '')) : uri_to.path
81
70
 
82
- # --all and --all-subscribed options override folders specified in URIs
83
- if options[:all_given] || options[:all_subscribed_given]
71
+ # --all and --all-subscribed options override folders
72
+ if config.all || config.all_subscribed
84
73
  uri_from.path = ''
85
74
  uri_to.path = ''
86
75
  end
87
76
 
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
77
  # 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]
78
+ uri_from.user = CGI.escape(config.from_user) if config.from_user
79
+ uri_from.password = CGI.escape(config.from_pass) if config.from_pass
80
+ uri_to.user = CGI.escape(config.to_user) if config.to_user
81
+ uri_to.password = CGI.escape(config.to_pass) if config.to_pass
99
82
 
100
- # If usernames/passwords aren't specified in either URIs or args, then prompt.
83
+ # If usernames/passwords aren't specified in either URIs or config, then prompt.
101
84
  uri_from.user ||= CGI.escape(ask("Source username (#{uri_from.host}): "))
102
85
  uri_from.password ||= CGI.escape(ask("Source password (#{uri_from.host}): ") {|q| q.echo = false })
103
86
  uri_to.user ||= CGI.escape(ask("Destination username (#{uri_to.host}): "))
104
87
  uri_to.password ||= CGI.escape(ask("Destination password (#{uri_to.host}): ") {|q| q.echo = false })
105
88
 
106
89
  # Go go go!
107
- init(
108
- options[:verbosity],
109
- options[:exclude] ? options[:exclude].flatten : [],
110
- options[:exclude_file]
111
- )
112
-
113
- Net::IMAP.debug = true if @log.level == :insane
90
+ init(config)
114
91
 
115
92
  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]
93
+ :dry_run => config[:dry_run],
94
+ :max_retries => config[:max_retries],
95
+ :ssl_certs => config[:ssl_certs] || nil,
96
+ :ssl_verify => config[:ssl_verify]
121
97
  )
122
98
 
123
99
  imap_to = Larch::IMAP.new(uri_to,
124
- :create_mailbox => !options[:no_create_folder] && !options[:dry_run],
125
- :dry_run => options[:dry_run],
126
- :fast_scan => options[:fast_scan],
127
- :max_retries => options[:max_retries],
128
- :ssl_certs => options[:ssl_certs] || nil,
129
- :ssl_verify => options[:ssl_verify]
100
+ :create_mailbox => !config[:no_create_folder] && !config[:dry_run],
101
+ :dry_run => config[:dry_run],
102
+ :max_retries => config[:max_retries],
103
+ :ssl_certs => config[:ssl_certs] || nil,
104
+ :ssl_verify => config[:ssl_verify]
130
105
  )
131
106
 
132
107
  unless RUBY_PLATFORM =~ /mswin|mingw|bccwin|wince|java/
@@ -138,9 +113,9 @@ EOS
138
113
  end
139
114
  end
140
115
 
141
- if options[:all_given]
116
+ if config.all
142
117
  copy_all(imap_from, imap_to)
143
- elsif options[:all_subscribed_given]
118
+ elsif config.all_subscribed
144
119
  copy_all(imap_from, imap_to, true)
145
120
  else
146
121
  copy_folder(imap_from, imap_to)
@@ -0,0 +1,105 @@
1
+ module Larch
2
+
3
+ class Config
4
+ attr_reader :filename, :section
5
+
6
+ DEFAULT = {
7
+ 'all' => false,
8
+ 'all-subscribed' => false,
9
+ 'config' => File.join('~', '.larch', 'config.yaml'),
10
+ 'database' => File.join('~', '.larch', 'larch.db'),
11
+ 'dry-run' => false,
12
+ 'exclude' => [],
13
+ 'exclude-file' => nil,
14
+ 'from' => nil,
15
+ 'from-folder' => 'INBOX',
16
+ 'from-pass' => nil,
17
+ 'from-user' => nil,
18
+ 'max-retries' => 3,
19
+ 'no-create-folder' => false,
20
+ 'ssl-certs' => nil,
21
+ 'ssl-verify' => false,
22
+ 'to' => nil,
23
+ 'to-folder' => 'INBOX',
24
+ 'to-pass' => nil,
25
+ 'to-user' => nil,
26
+ 'verbosity' => 'info'
27
+ }.freeze
28
+
29
+ def initialize(section = 'default', filename = DEFAULT['config'], override = {})
30
+ @section = section.to_s
31
+ @override = {}
32
+
33
+ override.each do |k, v|
34
+ opt = k.to_s.gsub('_', '-')
35
+ @override[opt] = v if DEFAULT.has_key?(opt) && override["#{k}_given".to_sym] && v != DEFAULT[opt]
36
+ end
37
+
38
+ load_file(filename)
39
+ end
40
+
41
+ def fetch(name)
42
+ (@cached || {})[name.to_s.gsub('_', '-')] || nil
43
+ end
44
+ alias [] fetch
45
+
46
+ def load_file(filename)
47
+ @filename = File.expand_path(filename)
48
+
49
+ config = {}
50
+
51
+ if File.exist?(@filename)
52
+ begin
53
+ config = YAML.load_file(@filename)
54
+ rescue => e
55
+ raise Larch::Config::Error, "config error in #{filename}: #{e}"
56
+ end
57
+ end
58
+
59
+ @lookup = [@override, config[@section] || {}, config['default'] || {}, DEFAULT]
60
+ cache_config
61
+ end
62
+
63
+ def method_missing(name)
64
+ fetch(name)
65
+ end
66
+
67
+ def validate
68
+ ['from', 'to'].each do |s|
69
+ raise Error, "'#{s}' must be a valid IMAP URI (e.g. imap://example.com)" unless fetch(s) =~ IMAP::REGEX_URI
70
+ end
71
+
72
+ unless Logger::LEVELS.has_key?(verbosity.to_sym)
73
+ raise Error, "'verbosity' must be one of: #{Logger::LEVELS.keys.join(', ')}"
74
+ end
75
+
76
+ if exclude_file
77
+ raise Error, "exclude file not found: #{exclude_file}" unless File.file?(exclude_file)
78
+ raise Error, "exclude file cannot be read: #{exclude_file}" unless File.readable?(exclude_file)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # Merges configs such that those earlier in the lookup chain override those
85
+ # later in the chain.
86
+ def cache_config
87
+ @cached = {}
88
+
89
+ @lookup.reverse.each do |c|
90
+ c.each {|k, v| @cached[k] = config_merge(@cached[k] || {}, v) }
91
+ end
92
+ end
93
+
94
+ def config_merge(master, value)
95
+ if value.is_a?(Hash)
96
+ value.each {|k, v| master[k] = config_merge(master[k] || {}, v) }
97
+ return master
98
+ end
99
+
100
+ value
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,12 @@
1
+ module Larch; module Database
2
+
3
+ class Account < Sequel::Model
4
+ plugin :hook_class_methods
5
+ one_to_many :mailboxes, :class => Larch::Database::Mailbox
6
+
7
+ before_destroy do
8
+ Mailbox.filter(:account_id => id).destroy
9
+ end
10
+ end
11
+
12
+ end; end
@@ -0,0 +1,12 @@
1
+ module Larch; module Database
2
+
3
+ class Mailbox < Sequel::Model
4
+ plugin :hook_class_methods
5
+ one_to_many :messages, :class => Larch::Database::Message
6
+
7
+ before_destroy do
8
+ Larch::Database::Message.filter(:mailbox_id => id).destroy
9
+ end
10
+ end
11
+
12
+ end; end
@@ -0,0 +1,6 @@
1
+ module Larch; module Database
2
+
3
+ class Message < Sequel::Model
4
+ end
5
+
6
+ end; end
@@ -0,0 +1,42 @@
1
+ class CreateSchema < Sequel::Migration
2
+ def down
3
+ drop_table :accounts, :mailboxes, :messages
4
+ end
5
+
6
+ def up
7
+ create_table :accounts do
8
+ primary_key :id
9
+ text :hostname, :null => false
10
+ text :username, :null => false
11
+
12
+ unique [:hostname, :username]
13
+ end
14
+
15
+ create_table :mailboxes do
16
+ primary_key :id
17
+ foreign_key :account_id, :table => :accounts
18
+ text :name, :null => false
19
+ text :delim, :null => false
20
+ text :attr, :null => false, :default => ''
21
+ integer :subscribed, :null => false, :default => 0
22
+ integer :uidvalidity
23
+ integer :uidnext
24
+
25
+ unique [:account_id, :name, :uidvalidity]
26
+ end
27
+
28
+ create_table :messages do
29
+ primary_key :id
30
+ foreign_key :mailbox_id, :table => :mailboxes
31
+ integer :uid, :null => false
32
+ text :guid, :null => false
33
+ text :message_id
34
+ integer :rfc822_size, :null => false
35
+ integer :internaldate, :null => false
36
+ text :flags, :null => false, :default => ''
37
+
38
+ index :guid
39
+ unique [:mailbox_id, :uid]
40
+ end
41
+ end
42
+ end
data/lib/larch/errors.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  module Larch
2
2
  class Error < StandardError; end
3
3
 
4
+ class Config
5
+ class Error < Larch::Error; end
6
+ end
7
+
4
8
  class IMAP
5
9
  class Error < Larch::Error; end
6
10
  class FatalError < Error; end