]> git.cworth.org Git - sup/blob - lib/sup.rb
Merge branch 'master' into next
[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     elsif o.respond_to? :before_marshal
92       o.before_marshal
93     else
94       o
95     end
96
97     if safe
98       safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
99       mode = File.stat(fn).mode if File.exists? fn
100       File.open(safe_fn, "w", mode) { |f| f.puts o.to_yaml }
101       FileUtils.mv safe_fn, fn
102     else
103       File.open(fn, "w") { |f| f.puts o.to_yaml }
104     end
105   end
106
107   def load_yaml_obj fn, compress=false
108     o = if File.exists? fn
109       if compress
110         Zlib::GzipReader.open(fn) { |f| YAML::load f }
111       else
112         YAML::load_file fn
113       end
114     end
115     if o.is_a?(Array)
116       o.each { |x| x.after_unmarshal! if x.respond_to?(:after_unmarshal!) }
117     else
118       o.after_unmarshal! if o.respond_to?(:after_unmarshal!)
119     end
120     o
121   end
122
123   def start
124     Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
125     Redwood::ContactManager.init Redwood::CONTACT_FN
126     Redwood::LabelManager.init Redwood::LABEL_FN
127     Redwood::AccountManager.init $config[:accounts]
128     Redwood::DraftManager.init Redwood::DRAFT_DIR
129     Redwood::UpdateManager.init
130     Redwood::PollManager.init
131     Redwood::CryptoManager.init
132     Redwood::UndoManager.init
133     Redwood::SourceManager.init
134   end
135
136   def finish
137     Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
138     Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
139     Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
140   end
141
142   ## not really a good place for this, so I'll just dump it here.
143   ##
144   ## a source error is either a FatalSourceError or an OutOfSyncSourceError.
145   ## the superclass SourceError is just a generic.
146   def report_broken_sources opts={}
147     return unless BufferManager.instantiated?
148
149     broken_sources = SourceManager.sources.select { |s| s.error.is_a? FatalSourceError }
150     unless broken_sources.empty?
151       BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do
152         TextMode.new(<<EOM)
153 Source error notification
154 -------------------------
155
156 Hi there. It looks like one or more message sources is reporting
157 errors. Until this is corrected, messages from these sources cannot
158 be viewed, and new messages will not be detected.
159
160 #{broken_sources.map { |s| "Source: " + s.to_s + "\n Error: " + s.error.message.wrap(70).join("\n        ")}.join("\n\n")}
161 EOM
162 #' stupid ruby-mode
163       end
164     end
165
166     desynced_sources = SourceManager.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
167     unless desynced_sources.empty?
168       BufferManager.spawn_unless_exists("Out-of-sync source notification for #{broken_sources.join(',')}", opts) do
169         TextMode.new(<<EOM)
170 Out-of-sync source notification
171 -------------------------------
172
173 Hi there. It looks like one or more sources has fallen out of sync
174 with my index. This can happen when you modify these sources with
175 other email clients. (Sorry, I don't play well with others.)
176
177 Until this is corrected, messages from these sources cannot be viewed,
178 and new messages will not be detected. Luckily, this is easy to correct!
179
180 #{desynced_sources.map do |s|
181   "Source: " + s.to_s + 
182    "\n Error: " + s.error.message.wrap(70).join("\n        ") + 
183    "\n   Fix: sup-sync --changed #{s.to_s}"
184   end}
185 EOM
186 #' stupid ruby-mode
187       end
188     end
189   end
190
191   module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
192                   :report_broken_sources
193 end
194
195 ## set up default configuration file
196 if File.exists? Redwood::CONFIG_FN
197   $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
198   abort "#{Redwood::CONFIG_FN} is not a valid configuration file (it's a #{$config.class}, not a hash)" unless $config.is_a?(Hash)
199 else
200   require 'etc'
201   require 'socket'
202   name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
203   name ||= ENV["USER"]
204   email = ENV["USER"] + "@" + 
205     begin
206       Socket.gethostbyname(Socket.gethostname).first
207     rescue SocketError
208       Socket.gethostname
209     end
210
211   $config = {
212     :accounts => {
213       :default => {
214         :name => name,
215         :email => email,
216         :alternates => [],
217         :sendmail => "/usr/sbin/sendmail -oem -ti",
218         :signature => File.join(ENV["HOME"], ".signature")
219       }
220     },
221     :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
222     :thread_by_subject => false,
223     :edit_signature => false,
224     :ask_for_cc => true,
225     :ask_for_bcc => false,
226     :ask_for_subject => true,
227     :confirm_no_attachments => true,
228     :confirm_top_posting => true,
229     :discard_snippets_from_encrypted_messages => false,
230     :default_attachment_save_dir => "",
231     :sent_source => "sup://sent"
232   }
233   begin
234     FileUtils.mkdir_p Redwood::BASE_DIR
235     Redwood::save_yaml_obj $config, Redwood::CONFIG_FN
236   rescue StandardError => e
237     $stderr.puts "warning: #{e.message}"
238   end
239 end
240
241 require "sup/util"
242 require "sup/hook"
243
244 ## we have to initialize this guy first, because other classes must
245 ## reference it in order to register hooks, and they do that at parse
246 ## time.
247 Redwood::HookManager.init Redwood::HOOK_DIR
248
249 ## everything we need to get logging working
250 require "sup/logger"
251 Redwood::Logger.init.add_sink $stderr
252 include Redwood::LogsStuff
253
254 ## determine encoding and character set
255   $encoding = Locale.current.charset
256   if $encoding
257     debug "using character set encoding #{$encoding.inspect}"
258   else
259     warn "can't find character set by using locale, defaulting to utf-8"
260     $encoding = "UTF-8"
261   end
262
263 require "sup/buffer"
264 require "sup/keymap"
265 require "sup/mode"
266 require "sup/modes/scroll-mode"
267 require "sup/modes/text-mode"
268 require "sup/modes/log-mode"
269 require "sup/update"
270 require "sup/message-chunks"
271 require "sup/message"
272 require "sup/source"
273 require "sup/mbox"
274 require "sup/maildir"
275 require "sup/imap"
276 require "sup/person"
277 require "sup/account"
278 require "sup/thread"
279 require "sup/interactive-lock"
280 require "sup/index"
281 require "sup/textfield"
282 require "sup/colormap"
283 require "sup/label"
284 require "sup/contact"
285 require "sup/tagger"
286 require "sup/draft"
287 require "sup/poll"
288 require "sup/crypto"
289 require "sup/undo"
290 require "sup/horizontal-selector"
291 require "sup/modes/line-cursor-mode"
292 require "sup/modes/help-mode"
293 require "sup/modes/edit-message-mode"
294 require "sup/modes/compose-mode"
295 require "sup/modes/resume-mode"
296 require "sup/modes/forward-mode"
297 require "sup/modes/reply-mode"
298 require "sup/modes/label-list-mode"
299 require "sup/modes/contact-list-mode"
300 require "sup/modes/thread-view-mode"
301 require "sup/modes/thread-index-mode"
302 require "sup/modes/label-search-results-mode"
303 require "sup/modes/search-results-mode"
304 require "sup/modes/person-search-results-mode"
305 require "sup/modes/inbox-mode"
306 require "sup/modes/buffer-list-mode"
307 require "sup/modes/poll-mode"
308 require "sup/modes/file-browser-mode"
309 require "sup/modes/completion-mode"
310 require "sup/modes/console-mode"
311 require "sup/sent"
312
313 $:.each do |base|
314   d = File.join base, "sup/share/modes/"
315   Redwood::Mode.load_all_modes d if File.directory? d
316 end