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