texmailer 0.1 → 0.2.d

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.
@@ -0,0 +1,126 @@
1
+ require 'net/imap'
2
+ require 'thread'
3
+ require 'timeout'
4
+ require 'texmailer/drafts'
5
+ require 'texmailer/adbook'
6
+
7
+ module Texmailer
8
+
9
+ CACHEVERSION = '0.1'
10
+
11
+ class Iview
12
+
13
+ def initialize userdir, maxmsg = 100
14
+ @userdir = userdir
15
+ @maxmsg = maxmsg
16
+ @msglock = Mutex.new
17
+ @emsg = nil
18
+ @dienow = false
19
+ @tid = nil
20
+ end
21
+
22
+ def running
23
+ return (@tid && @tid.status) ? true : false
24
+ end
25
+
26
+ def getsettings
27
+ stfile = @userdir+'/imap.cfg'
28
+ return {} if ! File.readable? stfile
29
+ return Marshal.load(File.read(stfile))
30
+ end
31
+
32
+ def savesettings iset
33
+ stdat = Marshal.dump(iset)
34
+ File.open(@userdir+'/imap.cfg','w',0600) {|f| f.write stdat}
35
+ end
36
+
37
+ def upabook
38
+ a,b = getcache
39
+ $ab.upenv(b.values.collect {|x| x[0]})
40
+ end
41
+
42
+ def getcache
43
+ c = [0,{}]
44
+ cfile = @userdir+'/imapcache.dat'
45
+ return 0,{} if ! File.readable? cfile
46
+ @msglock.synchronize { c = Marshal.load(File.read(cfile)) }
47
+ return 0,{} unless c[0] == CACHEVERSION
48
+ return c[1], c[2]
49
+ end
50
+
51
+ def stop
52
+ if running then
53
+ @dienow = true
54
+ @tid.join(10)
55
+ end
56
+ @dienow = false
57
+ end
58
+
59
+ def check
60
+ return nil if running
61
+ rval = @emsg
62
+ @emsg = nil
63
+ return nil if rval == "OK"
64
+ return rval
65
+ end
66
+
67
+ def pgets p,tout
68
+ s = nil
69
+ for i in (1..tout)
70
+ break if (s = IO.select([p],nil,nil,1)) != nil || @dienow
71
+ end
72
+ if s
73
+ return p.gets
74
+ else
75
+ p.close
76
+ return nil
77
+ end
78
+ end
79
+
80
+ def run
81
+ if ! running && ! @emsg
82
+ worker = File.expand_path(File.dirname(__FILE__)) + "/iworker"
83
+ p = IO.popen(worker+" "+@userdir+" "+String(@maxmsg),"r+")
84
+ Process.detach p.pid
85
+ stat = pgets p,5
86
+ return "Invalid IMAP settings" if ! stat
87
+ return stat if stat != "OK\n"
88
+ @tid = Thread.new(p) {|p| asyncget(p)}
89
+ @tid.join(4)
90
+ end
91
+
92
+ return check
93
+ end
94
+
95
+ def asyncget p
96
+ while true
97
+ stat = pgets p,30
98
+ if ! stat
99
+ @emsg = 'IMAP Fetch failed'
100
+ break
101
+ end
102
+ if stat == "EXIT\n"
103
+ break
104
+ end
105
+ if stat != "OK\n"
106
+ @emsg = stat
107
+ break
108
+ end
109
+ @msglock.synchronize {
110
+ p.puts "OK"
111
+ p.flush
112
+ stat = pgets p,5
113
+ }
114
+ if !stat || stat != "OK\n"
115
+ @emsg = 'IMAP Fetch failed'
116
+ break
117
+ end
118
+ end
119
+ p.close
120
+ @emsg ||= 'OK'
121
+ Thread.exit
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'net/imap'
4
+ require 'net/smtp'
5
+
6
+ CACHEVERSION = '0.1'
7
+
8
+ #Monkey Patch IMAP reader
9
+ module Net
10
+ class IMAP
11
+ class ResponseParser
12
+ def body_fields
13
+ param, content_id, desc, enc, size = nil, nil, nil, nil, nil
14
+
15
+ param = body_fld_param
16
+
17
+ token = lookahead
18
+ return param, content_id, desc, enc, size if token.symbol == T_RPAR
19
+ match(T_SPACE)
20
+ content_id = nstring
21
+
22
+ token = lookahead
23
+ return param, content_id, desc, enc, size if token.symbol == T_RPAR
24
+ match(T_SPACE)
25
+ desc = nstring
26
+
27
+ token = lookahead
28
+ return param, content_id, desc, enc, size if token.symbol == T_RPAR
29
+ match(T_SPACE)
30
+ enc = case_insensitive_string
31
+
32
+ token = lookahead
33
+ return param, content_id, desc, enc, size if token.symbol == T_RPAR
34
+ match(T_SPACE)
35
+ size = number
36
+ return param, content_id, desc, enc, size
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ $userdir = ARGV[0]
43
+ $maxmsg = Integer(ARGV[1])
44
+
45
+ stfile = $userdir+'/imap.cfg'
46
+ if ! File.readable? stfile
47
+ puts "Invalid IMAP settings"
48
+ exit
49
+ end
50
+
51
+ def savecache(uidv,cache)
52
+ s = Marshal.dump([CACHEVERSION, uidv, cache])
53
+ puts "OK"
54
+ $stdout.flush
55
+ while(!$stdin.eof)
56
+ break if g = STDIN.gets
57
+ end
58
+ exit if ! g
59
+ File.open($userdir+'/imapcache.dat','w',0600){|f| f.write s}
60
+ puts "OK"
61
+ end
62
+
63
+ def getcache
64
+ cfile = $userdir+'/imapcache.dat'
65
+ return 0,{} if ! File.readable? cfile
66
+ c = Marshal.load(File.read(cfile))
67
+ return 0,{} unless c[0] == CACHEVERSION
68
+ return c[1], c[2]
69
+ end
70
+
71
+ def findmime s,t,st
72
+ if s.media_type == t && s.subtype == st
73
+ return [],s.encoding,s.description
74
+ end
75
+
76
+ if s.media_type == 'MULTIPART'
77
+ for i in (0..(s.parts.length-1))
78
+ f,e,d = findmime(s.parts[i],t,st)
79
+ if f
80
+ return [String(i+1)] + f,e,d
81
+ end
82
+ end
83
+ return nil,nil,nil
84
+ else
85
+ return nil,nil,nil
86
+ end
87
+ end
88
+
89
+ iset = Marshal.load(File.read(stfile))
90
+ s_uidv,s_cache = getcache
91
+ # Release settings lock in calling thread
92
+ puts "OK"
93
+ $stdout.flush
94
+
95
+ begin
96
+ h = nil
97
+ certpath = File.expand_path(File.dirname(__FILE__))+'/ca-bundle.crt'
98
+ Timeout.timeout(15) {h = Net::IMAP.new(iset["host"],iset["port"],iset["ssl"],certpath,iset["igcert"])}
99
+ h.login(iset["user"],iset["password"])
100
+ h.examine "INBOX"
101
+ uidv = h.responses["UIDVALIDITY"][-1]
102
+ uids = h.uid_search(["ALL"]).reverse!
103
+ uids = uids[0..($maxmsg-1)] if uids.length > $maxmsg
104
+ cache = {}
105
+ uids.each { |uid| cache[uid] = s_cache[uid] if s_cache[uid] } if s_uidv == uidv
106
+ uids.each { |uid|
107
+ if ! cache[uid]
108
+ evl = h.uid_fetch(uid,["ENVELOPE"])[0].attr["ENVELOPE"]
109
+ res = h.uid_fetch(uid,['BODYSTRUCTURE'])[0].attr["BODYSTRUCTURE"]
110
+ id,enc,desc = findmime(res,'TEXT','PLAIN')
111
+ if id
112
+ attr = id.join('.') if id.length > 0
113
+ attr ||= '1'
114
+ attr = 'BODY['+attr+']'
115
+ body = h.uid_fetch(uid,[attr])[0].attr[attr]
116
+ body = body.unpack('M')[0] if enc.upcase == 'QUOTED-PRINTABLE'
117
+ body = body.unpack('m')[0] if enc.upcase == 'BASE64'
118
+ if desc && desc.rindex('TeXMailer')
119
+ texrep = true
120
+ end
121
+ end
122
+ body ||= ''
123
+ texrep ||= false
124
+ body.gsub!("\r\n","\n")
125
+ cache[uid] = [evl, [texrep, body]]
126
+ savecache uidv,cache
127
+ end
128
+ }
129
+
130
+ h.disconnect
131
+ puts "EXIT"
132
+ $stdout.flush
133
+
134
+ rescue Exception, Timeout::Error
135
+ puts 'IMAP Error: ' + $!.message
136
+ exit
137
+ end
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Ajax Autocomplete for jQuery, version 1.1.3
3
+ * (c) 2010 Tomas Kirda
4
+ *
5
+ * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
6
+ * For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
7
+ *
8
+ * Last Review: 04/19/2010
9
+ */
10
+
11
+ /*jslint onevar: true, evil: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
12
+ /*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
13
+
14
+ (function($) {
15
+
16
+ var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g');
17
+
18
+ function fnFormatResult(value, data, currentValue) {
19
+ var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
20
+ return value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
21
+ }
22
+
23
+ function Autocomplete(el, options) {
24
+ this.el = $(el);
25
+ this.el.attr('autocomplete', 'off');
26
+ this.suggestions = [];
27
+ this.data = [];
28
+ this.badQueries = [];
29
+ this.selectedIndex = -1;
30
+ this.currentValue = this.el.val();
31
+ this.intervalId = 0;
32
+ this.cachedResponse = [];
33
+ this.onChangeInterval = null;
34
+ this.ignoreValueChange = false;
35
+ this.serviceUrl = options.serviceUrl;
36
+ this.isLocal = false;
37
+ this.options = {
38
+ autoSubmit: false,
39
+ minChars: 1,
40
+ maxHeight: 300,
41
+ deferRequestBy: 0,
42
+ width: 0,
43
+ highlight: true,
44
+ params: {},
45
+ fnFormatResult: fnFormatResult,
46
+ delimiter: null,
47
+ zIndex: 9999
48
+ };
49
+ this.initialize();
50
+ this.setOptions(options);
51
+ }
52
+
53
+ $.fn.autocomplete = function(options) {
54
+ return new Autocomplete(this.get(0)||$('<input />'), options);
55
+ };
56
+
57
+
58
+ Autocomplete.prototype = {
59
+
60
+ killerFn: null,
61
+
62
+ initialize: function() {
63
+
64
+ var me, uid, autocompleteElId;
65
+ me = this;
66
+ uid = Math.floor(Math.random()*0x100000).toString(16);
67
+ autocompleteElId = 'Autocomplete_' + uid;
68
+
69
+ this.killerFn = function(e) {
70
+ if ($(e.target).parents('.autocomplete').size() === 0) {
71
+ me.killSuggestions();
72
+ me.disableKillerFn();
73
+ }
74
+ };
75
+
76
+ if (!this.options.width) { this.options.width = this.el.width(); }
77
+ this.mainContainerId = 'AutocompleteContainter_' + uid;
78
+
79
+ $('<div id="' + this.mainContainerId + '" style="position:absolute;z-index:9999;"><div class="autocomplete-w1"><div class="autocomplete" id="' + autocompleteElId + '" style="display:none; width:300px;"></div></div></div>').appendTo('body');
80
+
81
+ this.container = $('#' + autocompleteElId);
82
+ this.fixPosition();
83
+ if (window.opera) {
84
+ this.el.keypress(function(e) { me.onKeyPress(e); });
85
+ } else {
86
+ this.el.keydown(function(e) { me.onKeyPress(e); });
87
+ }
88
+ this.el.keyup(function(e) { me.onKeyUp(e); });
89
+ this.el.blur(function() { me.enableKillerFn(); });
90
+ this.el.focus(function() { me.fixPosition(); });
91
+ },
92
+
93
+ setOptions: function(options){
94
+ var o = this.options;
95
+ $.extend(o, options);
96
+ if(o.lookup){
97
+ this.isLocal = true;
98
+ if($.isArray(o.lookup)){ o.lookup = { suggestions:o.lookup, data:[] }; }
99
+ }
100
+ $('#'+this.mainContainerId).css({ zIndex:o.zIndex });
101
+ this.container.css({ maxHeight: o.maxHeight + 'px', width:o.width });
102
+ },
103
+
104
+ clearCache: function(){
105
+ this.cachedResponse = [];
106
+ this.badQueries = [];
107
+ },
108
+
109
+ disable: function(){
110
+ this.disabled = true;
111
+ },
112
+
113
+ enable: function(){
114
+ this.disabled = false;
115
+ },
116
+
117
+ fixPosition: function() {
118
+ var offset = this.el.offset();
119
+ $('#' + this.mainContainerId).css({ top: (offset.top + this.el.innerHeight()) + 'px', left: offset.left + 'px' });
120
+ },
121
+
122
+ enableKillerFn: function() {
123
+ var me = this;
124
+ $(document).bind('click', me.killerFn);
125
+ },
126
+
127
+ disableKillerFn: function() {
128
+ var me = this;
129
+ $(document).unbind('click', me.killerFn);
130
+ },
131
+
132
+ killSuggestions: function() {
133
+ var me = this;
134
+ this.stopKillSuggestions();
135
+ this.intervalId = window.setInterval(function() { me.hide(); me.stopKillSuggestions(); }, 300);
136
+ },
137
+
138
+ stopKillSuggestions: function() {
139
+ window.clearInterval(this.intervalId);
140
+ },
141
+
142
+ onKeyPress: function(e) {
143
+ if (this.disabled || !this.enabled) { return; }
144
+ // return will exit the function
145
+ // and event will not be prevented
146
+ switch (e.keyCode) {
147
+ case 27: //KEY_ESC:
148
+ this.el.val(this.currentValue);
149
+ this.hide();
150
+ break;
151
+ case 9: //KEY_TAB:
152
+ case 13: //KEY_RETURN:
153
+ if (this.selectedIndex === -1) {
154
+ this.hide();
155
+ return;
156
+ }
157
+ this.select(this.selectedIndex);
158
+ if(e.keyCode === 9){ return; }
159
+ break;
160
+ case 38: //KEY_UP:
161
+ this.moveUp();
162
+ break;
163
+ case 40: //KEY_DOWN:
164
+ this.moveDown();
165
+ break;
166
+ default:
167
+ return;
168
+ }
169
+ e.stopImmediatePropagation();
170
+ e.preventDefault();
171
+ },
172
+
173
+ onKeyUp: function(e) {
174
+ if(this.disabled){ return; }
175
+ switch (e.keyCode) {
176
+ case 38: //KEY_UP:
177
+ case 40: //KEY_DOWN:
178
+ return;
179
+ }
180
+ clearInterval(this.onChangeInterval);
181
+ if (this.currentValue !== this.el.val()) {
182
+ if (this.options.deferRequestBy > 0) {
183
+ // Defer lookup in case when value changes very quickly:
184
+ var me = this;
185
+ this.onChangeInterval = setInterval(function() { me.onValueChange(); }, this.options.deferRequestBy);
186
+ } else {
187
+ this.onValueChange();
188
+ }
189
+ }
190
+ },
191
+
192
+ onValueChange: function() {
193
+ clearInterval(this.onChangeInterval);
194
+ this.currentValue = this.el.val();
195
+ var q = this.getQuery(this.currentValue);
196
+ this.selectedIndex = -1;
197
+ if (this.ignoreValueChange) {
198
+ this.ignoreValueChange = false;
199
+ return;
200
+ }
201
+ if (q === '' || q.length < this.options.minChars) {
202
+ this.hide();
203
+ } else {
204
+ this.getSuggestions(q);
205
+ }
206
+ },
207
+
208
+ getQuery: function(val) {
209
+ var d, arr;
210
+ d = this.options.delimiter;
211
+ if (!d) { return $.trim(val); }
212
+ arr = val.split(d);
213
+ return $.trim(arr[arr.length - 1]);
214
+ },
215
+
216
+ getSuggestionsLocal: function(q) {
217
+ var ret, arr, len, val, i;
218
+ arr = this.options.lookup;
219
+ len = arr.suggestions.length;
220
+ ret = { suggestions:[], data:[] };
221
+ q = q.toLowerCase();
222
+ for(i=0; i< len; i++){
223
+ val = arr.suggestions[i];
224
+ if(val.toLowerCase().indexOf(q) === 0){
225
+ ret.suggestions.push(val);
226
+ ret.data.push(arr.data[i]);
227
+ }
228
+ }
229
+ return ret;
230
+ },
231
+
232
+ getSuggestions: function(q) {
233
+ var cr, me;
234
+ cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q];
235
+ if (cr && $.isArray(cr.suggestions)) {
236
+ this.suggestions = cr.suggestions;
237
+ this.data = cr.data;
238
+ this.suggest();
239
+ } else if (!this.isBadQuery(q)) {
240
+ me = this;
241
+ me.options.params.query = q;
242
+ $.get(this.serviceUrl, me.options.params, function(txt) { me.processResponse(txt); }, 'text');
243
+ }
244
+ },
245
+
246
+ isBadQuery: function(q) {
247
+ var i = this.badQueries.length;
248
+ while (i--) {
249
+ if (q.indexOf(this.badQueries[i]) === 0) { return true; }
250
+ }
251
+ return false;
252
+ },
253
+
254
+ hide: function() {
255
+ this.enabled = false;
256
+ this.selectedIndex = -1;
257
+ this.container.hide();
258
+ },
259
+
260
+ suggest: function() {
261
+ if (this.suggestions.length === 0) {
262
+ this.hide();
263
+ return;
264
+ }
265
+
266
+ var me, len, div, f, v, i, s, mOver, mClick;
267
+ me = this;
268
+ len = this.suggestions.length;
269
+ f = this.options.fnFormatResult;
270
+ v = this.getQuery(this.currentValue);
271
+ mOver = function(xi) { return function() { me.activate(xi); }; };
272
+ mClick = function(xi) { return function() { me.select(xi); }; };
273
+ this.container.hide().empty();
274
+ for (i = 0; i < len; i++) {
275
+ s = this.suggestions[i];
276
+ div = $((me.selectedIndex === i ? '<div class="selected"' : '<div') + ' title="' + s.replace(/\"/gi,'') + '">' + f(s, this.data[i], v) + '</div>');
277
+ div.mouseover(mOver(i));
278
+ div.click(mClick(i));
279
+ this.container.append(div);
280
+ }
281
+ this.enabled = true;
282
+ this.container.show();
283
+ },
284
+
285
+ processResponse: function(text) {
286
+ var response;
287
+ try {
288
+ response = eval('(' + text + ')');
289
+ } catch (err) { return; }
290
+ if (!$.isArray(response.data)) { response.data = []; }
291
+ if(!this.options.noCache){
292
+ this.cachedResponse[response.query] = response;
293
+ if (response.suggestions.length === 0) { this.badQueries.push(response.query); }
294
+ }
295
+ if (response.query === this.getQuery(this.currentValue)) {
296
+ this.suggestions = response.suggestions;
297
+ this.data = response.data;
298
+ this.suggest();
299
+ }
300
+ },
301
+
302
+ activate: function(index) {
303
+ var divs, activeItem;
304
+ divs = this.container.children();
305
+ // Clear previous selection:
306
+ if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
307
+ $(divs.get(this.selectedIndex)).removeClass();
308
+ }
309
+ this.selectedIndex = index;
310
+ if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
311
+ activeItem = divs.get(this.selectedIndex);
312
+ $(activeItem).addClass('selected');
313
+ }
314
+ return activeItem;
315
+ },
316
+
317
+ deactivate: function(div, index) {
318
+ div.className = '';
319
+ if (this.selectedIndex === index) { this.selectedIndex = -1; }
320
+ },
321
+
322
+ select: function(i) {
323
+ var selectedValue, f;
324
+ selectedValue = this.suggestions[i];
325
+ if (selectedValue) {
326
+ this.el.val(selectedValue);
327
+ if (this.options.autoSubmit) {
328
+ f = this.el.parents('form');
329
+ if (f.length > 0) { f.get(0).submit(); }
330
+ }
331
+ this.ignoreValueChange = true;
332
+ this.hide();
333
+ this.onSelect(i);
334
+ }
335
+ },
336
+
337
+ moveUp: function() {
338
+ if (this.selectedIndex === -1) { return; }
339
+ if (this.selectedIndex === 0) {
340
+ this.container.children().get(0).className = '';
341
+ this.selectedIndex = -1;
342
+ this.el.val(this.currentValue);
343
+ return;
344
+ }
345
+ this.adjustScroll(this.selectedIndex - 1);
346
+ },
347
+
348
+ moveDown: function() {
349
+ if (this.selectedIndex === (this.suggestions.length - 1)) { return; }
350
+ this.adjustScroll(this.selectedIndex + 1);
351
+ },
352
+
353
+ adjustScroll: function(i) {
354
+ var activeItem, offsetTop, upperBound, lowerBound;
355
+ activeItem = this.activate(i);
356
+ offsetTop = activeItem.offsetTop;
357
+ upperBound = this.container.scrollTop();
358
+ lowerBound = upperBound + this.options.maxHeight - 25;
359
+ if (offsetTop < upperBound) {
360
+ this.container.scrollTop(offsetTop);
361
+ } else if (offsetTop > lowerBound) {
362
+ this.container.scrollTop(offsetTop - this.options.maxHeight + 25);
363
+ }
364
+ this.el.val(this.getValue(this.suggestions[i]));
365
+ },
366
+
367
+ onSelect: function(i) {
368
+ var me, fn, s, d;
369
+ me = this;
370
+ fn = me.options.onSelect;
371
+ s = me.suggestions[i];
372
+ d = me.data[i];
373
+ me.el.val(me.getValue(s));
374
+ if ($.isFunction(fn)) { fn(s, d, me.el); }
375
+ },
376
+
377
+ getValue: function(value){
378
+ var del, currVal, arr, me;
379
+ me = this;
380
+ del = me.options.delimiter;
381
+ if (!del) { return value; }
382
+ currVal = me.currentValue;
383
+ arr = currVal.split(del);
384
+ if (arr.length === 1) { return value; }
385
+ return currVal.substr(0, currVal.length - arr[arr.length - 1].length) + value;
386
+ }
387
+
388
+ };
389
+
390
+ }(jQuery));