]> git.cworth.org Git - sup/blob - lib/sup.rb
2351fa586418448cc8efb7e91c60a82c95c4ae21
[sup] / lib / sup.rb
1 require 'rubygems'
2 require 'yaml'
3 require 'zlib'
4 require 'thread'
5 require 'fileutils'
6 require 'gettext'
7 require 'curses'
8 begin
9   require 'fastthread'
10 rescue LoadError
11 end
12
13 class Object
14   ## this is for debugging purposes because i keep calling #id on the
15   ## wrong object and i want it to throw an exception
16   def id
17     raise "wrong id called on #{self.inspect}"
18   end
19 end
20
21 class Module
22   def yaml_properties *props
23     props = props.map { |p| p.to_s }
24     vars = props.map { |p| "@#{p}" }
25     klass = self
26     path = klass.name.gsub(/::/, "/")
27
28     klass.instance_eval do
29       define_method(:to_yaml_properties) { vars }
30       define_method(:to_yaml_type) { "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}" }
31     end
32
33     YAML.add_domain_type("#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}", path) do |type, val|
34       klass.new(*props.map { |p| val[p] })
35     end
36   end
37 end
38
39 module Redwood
40   VERSION = "git"
41
42   BASE_DIR   = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
43   CONFIG_FN  = File.join(BASE_DIR, "config.yaml")
44   COLOR_FN   = File.join(BASE_DIR, "colors.yaml")
45   SOURCE_FN  = File.join(BASE_DIR, "sources.yaml")
46   LABEL_FN   = File.join(BASE_DIR, "labels.txt")
47   CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
48   DRAFT_DIR  = File.join(BASE_DIR, "drafts")
49   SENT_FN    = File.join(BASE_DIR, "sent.mbox")
50   LOCK_FN    = File.join(BASE_DIR, "lock")
51   SUICIDE_FN = File.join(BASE_DIR, "please-kill-yourself")
52   HOOK_DIR   = File.join(BASE_DIR, "hooks")
53
54   YAML_DOMAIN = "masanjin.net"
55   YAML_DATE = "2006-10-01"
56
57   DEFAULT_INDEX = 'ferret'
58
59   ## record exceptions thrown in threads nicely
60   @exceptions = []
61   @exception_mutex = Mutex.new
62
63   attr_reader :exceptions
64   def record_exception e, name
65     @exception_mutex.synchronize do
66       @exceptions ||= []
67       @exceptions << [e, name]
68     end
69   end
70
71   def reporting_thread name
72     if $opts[:no_threads]
73       yield
74     else
75       ::Thread.new do
76         begin
77           yield
78         rescue Exception => e
79           record_exception e, name
80         end
81       end
82     end
83   end
84
85   module_function :reporting_thread, :record_exception, :exceptions
86
87 ## one-stop shop for yamliciousness
88   def save_yaml_obj o, fn, safe=false
89     o = if o.is_a?(Array)
90       o.map { |x| (x.respond_to?(:before_marshal) && x.before_marshal) || x }
91     else
92       o.respond_to?(:before_marshal) && o.before_marshal
93     end
94
95     if safe
96       safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
97       mode = File.stat(fn).mode if File.exists? fn
98       File.open(safe_fn, "w", mode) { |f| f.puts o.to_yaml }
99       FileUtils.mv safe_fn, fn
100     else
101       File.open(fn, "w") { |f| f.puts o.to_yaml }
102     end
103   end
104
105   def load_yaml_obj fn, compress=false
106     o = if File.exists? fn
107       if compress
108         Zlib::GzipReader.open(fn) { |f| YAML::load f }
109       else
110         YAML::load_file fn
111       end
112     end
113     if o.is_a?(Array)
114       o.each { |x| x.after_unmarshal! if x.respond_to?(:after_unmarshal!) }
115     else
116       o.after_unmarshal! if o.respond_to?(:after_unmarshal!)
117     end
118     o
119   end
120
121   def start
122     Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
123     Redwood::ContactManager.init Redwood::CONTACT_FN
124     Redwood::LabelManager.init Redwood::LABEL_FN
125     Redwood::AccountManager.init $config[:accounts]
126     Redwood::DraftManager.init Redwood::DRAFT_DIR
127     Redwood::UpdateManager.init
128     Redwood::PollManager.init
129     Redwood::CryptoManager.init
130     Redwood::UndoManager.init
131     Redwood::SourceManager.init
132   end
133
134   def finish
135     Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
136     Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
137     Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
138   end
139
140   ## not really a good place for this, so I'll just dump it here.
141   ##
142   ## a source error is either a FatalSourceError or an OutOfSyncSourceError.
143   ## the superclass SourceError is just a generic.
144   def report_broken_sources opts={}
145     return unless BufferManager.instantiated?
146
147     broken_sources = SourceManager.sources.select { |s| s.error.is_a? FatalSourceError }
148     unless broken_sources.empty?
149       BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do
150         TextMode.new(<<EOM)
151 Source error notification
152 -------------------------
153
154 Hi there. It looks like one or more message sources is reporting
155 errors. Until this is corrected, messages from these sources cannot
156 be viewed, and new messages will not be detected.
157
158 #{broken_sources.map { |s| "Source: " + s.to_s + "\n Error: " + s.error.message.wrap(70).join("\n        ")}.join("\n\n")}
159 EOM
160 #' stupid ruby-mode
161       end
162     end
163
164     desynced_sources = SourceManager.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
165     unless desynced_sources.empty?
166       BufferManager.spawn_unless_exists("Out-of-sync source notification for #{broken_sources.join(',')}", opts) do
167         TextMode.new(<<EOM)
168 Out-of-sync source notification
169 -------------------------------
170
171 Hi there. It looks like one or more sources has fallen out of sync
172 with my index. This can happen when you modify these sources with
173 other email clients. (Sorry, I don't play well with others.)
174
175 Until this is corrected, messages from these sources cannot be viewed,
176 and new messages will not be detected. Luckily, this is easy to correct!
177
178 #{desynced_sources.map do |s|
179   "Source: " + s.to_s + 
180    "\n Error: " + s.error.message.wrap(70).join("\n        ") + 
181    "\n   Fix: sup-sync --changed #{s.to_s}"
182   end}
183 EOM
184 #' stupid ruby-mode
185       end
186     end
187   end
188
189   module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
190                   :report_broken_sources
191 end
192
193 ## set up default configuration file
194 if File.exists? Redwood::CONFIG_FN
195   $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
196   abort "#{Redwood::CONFIG_FN} is not a valid configuration file (it's a #{$config.class}, not a hash)" unless $config.is_a?(Hash)
197 else
198   require 'etc'
199   require 'socket'
200   name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
201   name ||= ENV["USER"]
202   email = ENV["USER"] + "@" + 
203     begin
204       Socket.gethostbyname(Socket.gethostname).first
205     rescue SocketError
206       Socket.gethostname
207     end
208
209   $config = {
210     :accounts => {
211       :default => {
212         :name => name,
213         :email => email,
214         :alternates => [],
215         :sendmail => "/usr/sbin/sendmail -oem -ti",
216         :signature => File.join(ENV["HOME"], ".signature")
217       }
218     },
219     :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
220     :thread_by_subject => false,
221     :edit_signature => false,
222     :ask_for_cc => true,
223     :ask_for_bcc => false,
224     :ask_for_subject => true,
225     :confirm_no_attachments => true,
226     :confirm_top_posting => true,
227     :discard_snippets_from_encrypted_messages => false,
228     :default_attachment_save_dir => "",
229     :sent_source => "sup://sent"
230   }
231   begin
232     FileUtils.mkdir_p Redwood::BASE_DIR
233     Redwood::save_yaml_obj $config, Redwood::CONFIG_FN
234   rescue StandardError => e
235     $stderr.puts "warning: #{e.message}"
236   end
237 end
238
239 require "sup/util"
240 require "sup/hook"
241
242 ## we have to initialize this guy first, because other classes must
243 ## reference it in order to register hooks, and they do that at parse
244 ## time.
245 Redwood::HookManager.init Redwood::HOOK_DIR
246
247 ## everything we need to get logging working
248 require "sup/logger"
249 Redwood::Logger.init.add_sink $stderr
250 include Redwood::LogsStuff
251
252 ## determine encoding and character set
253   $encoding = Locale.current.charset
254   if $encoding
255     debug "using character set encoding #{$encoding.inspect}"
256   else
257     warn "can't find character set by using locale, defaulting to utf-8"
258     $encoding = "UTF-8"
259   end
260
261 require "sup/buffer"
262 require "sup/keymap"
263 require "sup/mode"
264 require "sup/modes/scroll-mode"
265 require "sup/modes/text-mode"
266 require "sup/modes/log-mode"
267 require "sup/update"
268 require "sup/message-chunks"
269 require "sup/message"
270 require "sup/source"
271 require "sup/mbox"
272 require "sup/maildir"
273 require "sup/imap"
274 require "sup/person"
275 require "sup/account"
276 require "sup/thread"
277 require "sup/interactive-lock"
278 require "sup/index"
279 require "sup/textfield"
280 require "sup/colormap"
281 require "sup/label"
282 require "sup/contact"
283 require "sup/tagger"
284 require "sup/draft"
285 require "sup/poll"
286 require "sup/crypto"
287 require "sup/undo"
288 require "sup/horizontal-selector"
289 require "sup/modes/line-cursor-mode"
290 require "sup/modes/help-mode"
291 require "sup/modes/edit-message-mode"
292 require "sup/modes/compose-mode"
293 require "sup/modes/resume-mode"
294 require "sup/modes/forward-mode"
295 require "sup/modes/reply-mode"
296 require "sup/modes/label-list-mode"
297 require "sup/modes/contact-list-mode"
298 require "sup/modes/thread-view-mode"
299 require "sup/modes/thread-index-mode"
300 require "sup/modes/label-search-results-mode"
301 require "sup/modes/search-results-mode"
302 require "sup/modes/person-search-results-mode"
303 require "sup/modes/inbox-mode"
304 require "sup/modes/buffer-list-mode"
305 require "sup/modes/poll-mode"
306 require "sup/modes/file-browser-mode"
307 require "sup/modes/completion-mode"
308 require "sup/sent"
309
310 $:.each do |base|
311   d = File.join base, "sup/share/modes/"
312   Redwood::Mode.load_all_modes d if File.directory? d
313 end