larch 1.0.2 → 1.1.0dev20091006

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