-
Notifications
You must be signed in to change notification settings - Fork 1
/
client.py
executable file
·2537 lines (2172 loc) · 110 KB
/
client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#! /usr/bin/env python
# MailTask Alpha: The Email Manager
# Copyright (C) 2015 Patrick Simmons
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#Originally generated by flconvert
from collections import Counter,deque
from html2text import html2text
from request_structures import CaseInsensitiveDict,CaseInsensitiveDefaultDict
if __name__=="__main__":
from fltk import *
from FLTK_Advanced_Browser import *
else:
def fl_alert(msg):
print msg
def Fl_flush():
pass
class empty:
pass
ui = empty()
ui.block_w = empty()
ui.block_w.show = ui.block_w.hide = Fl_flush
if __name__=="__main__":
import mt_attache
import mt_utils
from ontask_messages import *
import base64
from copy import copy
import cPickle
import email
import email.message
import email.parser
import email.utils
import functools
from html2text import html2text
import os
import shutil
import socket
import sys
import time
from select import select
#This would be too annoying if it had a long name
gfind = mt_utils.find
CaseInsensitiveList = mt_utils.CaseInsensitiveList
##To use in user agent
PROG_VERSION_STRING="MailTask/20181227"
##Constant tuple containing all user-modifiable headers.
# Other headers are ignored.
MODIFIABLE_HEADERS = CaseInsensitiveList(["From","Reply-To","To","Cc","Bcc","Subject", "X-MailTask-Date-Info", "X-MailTask-Completion-Status", "X-MailTask-Priority", "Date"])
##Constant tuple containing headers likely to have an email address in them
EMAIL_MODIFIABLE_HEADERS = CaseInsensitiveList(["From","Reply-To","To","Cc","Bcc"])
##Constant tuple containing all headers that should not be auto-deleted when replyifying a message
NON_DELETE_HEADERS = CaseInsensitiveList(MODIFIABLE_HEADERS + CaseInsensitiveList(["References","MIME-Version","Content-Type"]))
##Account Info list
account_info = []
##Address book dictionary
address_book = {}
#These vars keep track of whether it's okay to update the address book
addrbook_update_time=0
addrbook_pong_received=True
##Initialize account info list
# Format of file: five lines per account
# 0. Username
# 1. Password
# 2. IMAP server
# 3. SMTP server
# 4. From Address String
# All we care about is from address string
def initialize_account_info():
aifile = open(os.path.join(cachedir,"ACCOUNT_INFO"))
lines = aifile.readlines()
lines = map(str.rstrip,lines)
for i in range(1,len(lines),5):
account_info.append(lines[i+4])
##Initialize address book dictionary
def initialize_addrbook():
global address_book
global addrbook_pong_received
address_book = {}
addrbook_pong_received=True
addrbookfile = open(os.path.join(cachedir,"ADDRESSBOOK"))
lines = addrbookfile.readlines()
lines = map(str.rstrip,lines)
for i in range(0,len(lines),2):
address_book[lines[i]] = set(lines[i+1].split(","))
##Get email address from an email header string
def get_email_addr_from_header(f_header):
components = f_header.split()
handled=False
for token in components:
if token.find('@')!=-1:
return token.lstrip('<').rstrip('>').lower()
return ""
##Get nickname from email header string
def get_nick_from_header(f_header):
if get_email_addr_from_header(f_header)!=f_header and f_header.find("<") not in (0,-1):
to_return = f_header[0:f_header.find("<")].strip().strip("'\"").strip()
if to_return!="":
return to_return
return None
##Returns True if any address book entry has the set containing only the passed string as its value
def addrbook_rev_email_set_cardinality_one_lookup(a_val):
for entry in address_book:
if address_book[entry]==set([a_val]):
return True
return False
##Return email address or addresses for address book entry
def addrbook_lookup(key):
if key not in address_book:
email = get_email_addr_from_header(key)
nick = get_nick_from_header(key)
if nick!=None and (nick not in address_book or email not in address_book[nick]) and nick.find(',')==-1 and nick.find('\n')==-1 and not addrbook_rev_email_set_cardinality_one_lookup(email) and nick!=email:
c_state.update_addr_book(nick,email)
return key
to_return=""
for entry in address_book[key]:
if to_return!="":
to_return+=","
to_return+=entry
return to_return
##Return address book entry/entries for email address/addresses
def addrbook_reverse_lookup(ikey):
to_return=""
bag_of_addresses = set()
for entry_ in mt_utils.decomma(ikey).split(","):
entry = mt_utils.recomma(entry_)
if entry in address_book:
bag_of_addresses |= address_book[entry]
else:
email = get_email_addr_from_header(entry)
bag_of_addresses.add(email)
nick = get_nick_from_header(entry)
if nick!=None and (nick not in address_book or email not in address_book[nick]) and nick.find(',')==-1 and nick.find('\n')==-1 and not addrbook_rev_email_set_cardinality_one_lookup(email) and nick!=email:
c_state.update_addr_book(nick,email)
while len(bag_of_addresses):
largest_subset=set()
lsubkey = None
for key in address_book:
if address_book[key] <= bag_of_addresses and len(address_book[key]) > len(largest_subset):
largest_subset = copy(address_book[key])
lsubkey=key
if len(largest_subset):
if to_return!="":
to_return+=","
to_return+=lsubkey
bag_of_addresses-=largest_subset
else:
break
for addr in bag_of_addresses:
if to_return!="":
to_return+=","
partial_nick=None
l_smallest_superset=sys.maxint
for entry in address_book:
if addr in address_book[entry] and len(address_book[entry]) < l_smallest_superset:
partial_nick = entry
l_smallest_superset = len(address_book[entry])
if partial_nick:
to_return+=partial_nick+" <"+addr+">"
else:
to_return+=addr
return to_return
##Get string appropriate for use in main editor, given a
# dictionary of the headers in an email message
def get_editor_str(current_headers):
current_headers.sort(key=lambda x: gfind(MODIFIABLE_HEADERS,x[0]) if gfind(MODIFIABLE_HEADERS,x[0])!=-1 else sys.maxint)
newtext=""
divide_reached=False
for header in current_headers:
if not divide_reached and header[0] not in MODIFIABLE_HEADERS:
divide_reached=True
newtext+="\n\n\n"
newtext+=header[0]+": "+header[1].replace("\r\n","\n")+"\n"
return newtext
##Given an email address header dictionary, return a
# pretty-printable string representing what to include
# in the From/To column of the message list.
# Despite the name of the parameter, the parameter may
# but need not be a default dictionary
def get_prettyprintable_column_str(dd_hdrs):
#print "in get_prettyprintable_column_str"
s_params = get_email_send_parameters_from_msg(dd_hdrs)
#Are we the sender?
if int(s_params[0])!=len(account_info): #yes
#Then we print the destination addresses
#Color dictionary
color_dict = { 4 : FL_DARK_GREEN, 5 : FL_DARK_BLUE, 6 : FL_DARK_BLUE,
7 : FL_DARK_BLUE, 2 : 89, 3 : FL_DARK_MAGENTA, 1 : FL_DARK_RED }
#Single octal number To/Cc/Bcc
color_selector = (("To" in dd_hdrs) << 2) + (("Cc" in dd_hdrs) << 1) + ("Bcc" in dd_hdrs)
#No recipients, no column value
if not color_selector:
return ""
#Otherwise, return appropriate colored string
best_header = dd_hdrs['To'] if 'To' in dd_hdrs else dd_hdrs['Cc'] if 'Cc' in dd_hdrs else dd_hdrs['Bcc']
return "@C"+repr(color_dict[color_selector])+"@."+mt_utils.get_unicode_subject(addrbook_reverse_lookup(best_header))
elif 'From' in dd_hdrs: #not the sender
return "@."+mt_utils.get_unicode_subject(addrbook_reverse_lookup(dd_hdrs['From']))
else:
return "@.Unknown"
def get_email_send_parameters_from_msg(msg):
#Make sure msg contains "From" key; abort if not
if "From" not in msg:
return (repr(len(account_info)),[])
#Get sending account id
i=0
while i<len(account_info):
if get_email_addr_from_header(msg["From"])==get_email_addr_from_header(account_info[i]):
break
i+=1
accid=repr(i)
#Create list of destination addrs
dest_addrs = []
for header in ("To","Cc","Bcc"):
if header in msg:
for address in mt_utils.decomma(msg[header]).split(","):
dest_addrs.append(get_email_addr_from_header(address))
return (accid, dest_addrs)
##Get rid of newlines etc. and make all email addresses separated by ","
def sanitize_msg_headers(msg):
for header in ("From","To","Cc","Bcc","Reply-To"):
if header in msg:
msg.replace_header(header,msg[header].replace("\r"," ").replace("\n"," ").replace("\t"," "))
class ClientUI:
#Enumeration of possible left_browser update causes
SELF_INDUCED=0
STACK_PUSHED=1
STACK_POPPED=2
UI_INDUCED=3
#Menus for emails/tasks
EMAIL_MENU = ("Headers","Body","Attachments","Related")
TASK_MENU = ("Metadata","Description","Drafts","Files","Related")
##left_browser_callback:
# - handle updating upper text display, lower text display, stack.
# - Create choices for left browser, select first one if choices changed
# - Update nsync.cache appropriately.
# - Refresh main browser, but not through main_browser_callback.
@staticmethod
def left_browser_callback(widget,cause=SELF_INDUCED):
FOLDER_VIEW=1
EMAIL_VIEW=2
TASK_VIEW=3
ATTACHMENT_VIEW=4
#UI-induced callbacks should often be ignored
if cause==ClientUI.UI_INDUCED and (not ui.left_browser.value() or ui.left_browser.value()==ui.lb_selected):
return 1
elif cause==ClientUI.UI_INDUCED:
cause=ClientUI.SELF_INDUCED
#Can't process click if main editor is open.
if ui.main_editor.visible():
if ui.left_browser.value()!=ui.lb_selected:
fl_alert("Illegal action: editor open.")
ui.left_browser.select(ui.lb_selected)
return 1
#Back up mb_selected in case we mess with main_browser later (likely)
ui.mb_selected = ui.main_browser.value()
#Are we displaying info in the left pane about a task or a message?
if len(c_state.stack) < 3:
current_view = FOLDER_VIEW
elif c_state.stack[-2][0]==ClientState.ATTACHMENTS:
current_view = ATTACHMENT_VIEW
elif c_state.stack[-2][0]==ClientState.MESSAGE and c_state.stack[-2][1].get_content_type()=="multipart/x.mailtask":
current_view = TASK_VIEW
else:
current_view = EMAIL_VIEW
#We were actually clicked or have been ordered to do a simple refresh.
if cause==ClientUI.SELF_INDUCED:
#Were we actually clicked? If so, handle thlat.
if ui.left_browser.value()!=ui.lb_selected:
#Save new selection value
ui.lb_selected = ui.left_browser.value()
#We need to update the stack to reflect the new selection
if ui.lb_selected!=0: #just ignore the click if it's a deselection
if current_view!=ATTACHMENT_VIEW:
c_state.stack.pop()
if current_view==FOLDER_VIEW:
c_state.stack.append((ClientState.FOLDER,ui.left_browser.text(ui.lb_selected)))
ui.main_browser.deselect() #necessary or get_stacktop_uidpath() won't work
elif current_view==EMAIL_VIEW:
select_id = ClientUI.EMAIL_MENU.index(ui.left_browser.text(ui.lb_selected))
if select_id==0:
c_state.stack.append((ClientState.HEADERS,))
elif select_id==1:
todisplay = mt_utils.get_body(c_state.stack[-1][1])
if todisplay:
c_state.stack.append((ClientState.SUBMESSAGE,todisplay))
else:
c_state.stack.append((ClientState.ATTACHMENTS,))
elif select_id==2:
c_state.stack.append((ClientState.ATTACHMENTS,))
else: #select_id==3
c_state.stack.append((ClientState.RELATED,))
elif current_view==TASK_VIEW:
select_id = ClientUI.TASK_MENU.index(ui.left_browser.text(ui.lb_selected))
if select_id==0:
c_state.stack.append((ClientState.HEADERS,))
elif select_id==1:
todisplay = mt_utils.get_body(c_state.stack[-1][1])
if todisplay:
c_state.stack.append((ClientState.SUBMESSAGE,todisplay))
else:
c_state.stack.append((ClientState.ATTACHMENTS,))
elif select_id==2:
c_state.stack.append((ClientState.DRAFTS,))
elif select_id==3:
c_state.stack.append((ClientState.ATTACHMENTS,))
else: #select_id==4
c_state.stack.append((ClientState.RELATED,))
#Refreshing main browser/display handled below
else: #pushed or popped: handle updating LB options set lb selection to 1
ui.left_browser.clear()
if current_view==FOLDER_VIEW:
for i in range(len(account_info)):
ui.left_browser.add(repr(i)+"/INBOX")
ui.left_browser.add(repr(i)+"/Sent")
ui.left_browser.add("Tasks")
elif current_view==EMAIL_VIEW:
for line in ClientUI.EMAIL_MENU:
ui.left_browser.add(line)
elif current_view==TASK_VIEW:
for line in ClientUI.TASK_MENU:
ui.left_browser.add(line)
else: #ATTACHMENT_VIEW
ui.left_browser.add("Attachment")
#Okay, now we just refresh the main display/browser and upper and
#lower main display/browser headers.
#Upper text display
ui.upper_text_display.label(c_state.stack[0][1] if c_state.get_stacktop_uidpath()==None else c_state.get_stacktop_uidpath())
#Lower text display
if current_view==FOLDER_VIEW:
if c_state.stack[0][1]=="Tasks":
ui.lower_text_display1.label("Title")
ui.lower_text_display2.label("Type")
else:
ui.lower_text_display1.label("Subject")
ui.lower_text_display1.label("From/To")
ui.lower_text_display3.label("Date")
elif c_state.stack[-1][0]==ClientState.ATTACHMENTS:
ui.lower_text_display1.label("Filename")
ui.lower_text_display2.label("Type")
ui.lower_text_display3.label("Date")
else:
ui.lower_text_display1.label("")
ui.lower_text_display2.label("")
ui.lower_text_display3.label("")
#Refresh main browser/display:
if c_state.stack[-1][0]==ClientState.FOLDER: #Email folder or task folder
ui.main_display.hide()
ui.main_browser.show()
ui.main_browser.clear()
horizon = ui.mb_selected + 100
mb_counter = 0
if c_state.stack[-1][1]=="Tasks":
for entry in nsync.cache[c_state.stack[-1][1]]:
if mb_counter>=horizon:
break
mb_counter+=1
if c_state.show_completed_tasks or entry[1]['X-MailTask-Completion-Status']!="Completed" or 'X-MailTask-Priority' in entry[1]:
tasktype = mt_utils.get_task_type(entry[1])
if tasktype=="Checklist":
dinfo = mt_utils.browser_time(entry[1]['Date'])
elif tasktype=="Deadline":
dinfo = "D:"+mt_utils.browser_time(entry[1]['X-MailTask-Date-Info'],"%m/%d/%y %H:%M")
else: #Meeting
mtimes = map(str.strip,entry[1]['X-MailTask-Date-Info'].split("/"))
mtime_s1 = 0
mtime_s2 = 0
if email.utils.parsedate_tz(mtimes[0])!=None:
mtime_s1 = time.localtime(email.utils.mktime_tz(email.utils.parsedate_tz(mtimes[0])))
if email.utils.parsedate_tz(mtimes[1])!=None:
mtime_s2 = time.localtime(email.utils.mktime_tz(email.utils.parsedate_tz(mtimes[1])))
ctime = time.localtime()
current_year = ctime.tm_year==mtime_s1.tm_year
current_month = ctime.tm_mon==mtime_s1.tm_mon
current_day = ctime.tm_mday==mtime_s1.tm_mday
match_year = mtime_s1.tm_year==mtime_s2.tm_year
match_month = mtime_s1.tm_mon==mtime_s2.tm_mon
match_day = mtime_s1.tm_mday==mtime_s2.tm_mday
match_hour = mtime_s1.tm_hour==mtime_s2.tm_hour
match_minute = mtime_s1.tm_min==mtime_s2.tm_min
fmatstr1 = "%m/%d/%y %H:%M"
if current_year:
fmatstr1 = fmatstr1.replace("/%y","")
if current_year and current_month:
fmatstr1 = fmatstr1.replace("%m/","")
if current_year and current_month and current_day:
fmatstr1 = fmatstr1.replace("%d ","")
fmatstr2 = "%m/%d/%y %H:%M"
if match_year:
fmatstr2 = fmatstr2.replace("/%y","")
if match_year and match_month:
fmatstr2 = fmatstr2.replace("%m/","")
if match_year and match_month and match_day:
fmatstr2 = fmatstr2.replace("%d ","")
if match_hour and match_minute:
fmatstr2 = fmatstr2.replace("%H:%M","")
elif match_hour:
fmatstr2 = fmatstr2.replace("%H","")
elif match_minute:
fmatstr2 = fmatstr2.replace("%M","")
fmatstr2 = fmatstr2.rstrip()
dinfo=time.strftime(fmatstr1,mtime_s1)+"-"+time.strftime(fmatstr2,mtime_s2)
#Highlight tasks with unsent drafts in purple,
#or else I'd doubt my commitment to Sparkle Motion.
prefix = "@."
if "SPARKLE-MOTION" in entry[1] and 'X-MailTask-Priority' in entry[1] and entry[1]['X-MailTask-Completion-Status']=="Completed":
prefix = "@C"+repr(FL_DARK_RED)+"@."
elif "SPARKLE-MOTION" in entry[1] and 'X-MailTask-Priority' in entry[1]:
prefix = "@b@C"+repr(FL_DARK_MAGENTA)+"@."
elif "SPARKLE-MOTION" in entry[1]:
prefix = "@C"+repr(FL_DARK_MAGENTA)+"@."
elif entry[1]['X-MailTask-Completion-Status']=="Completed" and 'X-MailTask-Priority' in entry[1]:
prefix = "@C"+repr(FL_DARK_CYAN)+"@."
elif 'X-MailTask-Priority' in entry[1] and int(entry[1]['X-MailTask-Priority']) > 1:
prefix = "@b@C"+repr(FL_RED)+"@."
elif 'X-MailTask-Priority' in entry[1]:
prefix = "@b@."
ui.main_browser.add(prefix+mt_utils.get_unicode_subject(entry[1]['Subject'])+"\t@."+tasktype+"\t@."+dinfo)
else: #non-Task folder
for entry in nsync.cache[c_state.stack[-1][1]]:
if mb_counter>=horizon:
break
mb_counter+=1
dd_hdrs = CaseInsensitiveDefaultDict(lambda: "", entry[1])
ui.main_browser.add("@."+mt_utils.get_unicode_subject(dd_hdrs["Subject"])+"\t"+get_prettyprintable_column_str(entry[1])+"\t@."+(mt_utils.browser_time(dd_hdrs["Date"]) if dd_hdrs["Date"]!="" else ""))
if ui.mb_selected<=len(nsync.cache[c_state.stack[-1][1]]):
ui.main_browser.select(ui.mb_selected)
else:
ui.main_browser.deselect()
ui.mb_selected=0
elif c_state.stack[-1][0]==ClientState.HEADERS: #viewing headers/metadata
ui.main_display.show()
ui.main_browser.hide()
#Refresh display
ui.main_buffer.text(get_editor_str(CaseInsensitiveList(c_state.stack[-2][1].items())))
elif c_state.stack[-1][0]==ClientState.ATTACHMENTS: #viewing attachments
ui.main_display.hide()
ui.main_browser.show()
#Refresh attachments
nsync.cache["ATTACHMENTS"] = []
##Processes single submsg containing an attachment
def process_single_submsg(component):
if 'Content-Disposition' in component and component['Content-Disposition'].split(";")[0].strip().lower()=="attachment":
adate=component.get_param("modification-date",0,"Content-Disposition")
elif "Date" in component:
adate=component["Date"]
else:
adate="Unknown"
adict = {}
adict[None]=component
for header in component.items():
adict[header[0]]=header[1]
nsync.cache["ATTACHMENTS"].append((adate,adict))
##Walks subtree of attachments, appends all to "ATTACHMENTS" key in cache
mt_utils.walk_attachments(c_state.stack[-2][1],process_single_submsg,True)
nsync.cache["ATTACHMENTS"].sort(key=lambda k: k[0],reverse=True)
#Update browser
ui.main_browser.clear()
for x in nsync.cache["ATTACHMENTS"]:
ui.main_browser.add("@."+x[1][None].get_param("filename","","Content-Disposition")+"\t@."+x[1][None].get_content_type()+"\t@."+mt_utils.browser_time(x[1][None].get_param("modification-date","","Content-Disposition")))
if ui.mb_selected<=len(nsync.cache["ATTACHMENTS"]):
ui.main_browser.select(ui.mb_selected)
else:
ui.main_browser.deselect()
ui.mb_selected=0
elif c_state.stack[-1][0]==ClientState.SUBMESSAGE: #viewing body of message or single attachment
ui.main_browser.hide()
ui.main_display.show()
#Extensible attachment handler
ui.main_buffer.text(mt_attache.display_submessage(c_state.stack[-1][1]).replace("\r\n","\n"))
elif c_state.stack[-1][0]==ClientState.RELATED:
ui.main_display.hide()
ui.main_browser.show()
#Related view. We CANNOT TRUST nsync.cache["RELATED"] here!
#That information MAY BE STALE. It is OUR JOB to update it!
#So, let's do that now.
#Get all folders, remove "RELATED", "DRAFTS", "ATTACHMENTS"
cachekeys = nsync.cache.keys()
for key in ("RELATED","DRAFTS","ATTACHMENTS"):
if key in cachekeys:
cachekeys.remove(key)
oldrelated = nsync.cache["RELATED"] if "RELATED" in nsync.cache else []
nsync.cache["RELATED"] = []
primary_msg = c_state.stack[-2][1]
if 'References' in primary_msg:
for rmid in mt_utils.get_related_ids(primary_msg):
record = mt_utils.search_cache(rmid,oldrelated)
if record!=None:
nsync.cache["RELATED"].append(record)
continue
for key in cachekeys:
record = mt_utils.search_cache(rmid,nsync.cache[key])
if record!=None:
record[1]["FOLDER"]=key
nsync.cache["RELATED"].append(record)
break
#Okay, now let's sort the RELATED list
nsync.cache["RELATED"].sort(key=lambda k: k[0],reverse=True)
#Update browser
ui.main_browser.clear()
for entry_ in nsync.cache["RELATED"]:
entry = CaseInsensitiveDefaultDict(lambda: "", entry_[1])
ui.main_browser.add("@."+mt_utils.get_unicode_subject(entry["Subject"])+"\t@."+entry["From"]+"\t@."+entry["Date"])
if ui.mb_selected<=len(nsync.cache["RELATED"]):
ui.main_browser.select(ui.mb_selected)
else:
ui.main_browser.deselect()
ui.mb_selected=0
else: #c_state.stack[-1][0]==ClientState.DRAFTS
ui.main_display.hide()
ui.main_browser.show()
#Drafts view. We CANNOT TRUST nsync.cache["DRAFTS"] here!
#That information MAY BE STALE. It is OUR JOB to update it!
#So, let's do that now.
nsync.cache["DRAFTS"]=[]
primary_msg = c_state.stack[-2][1]
for submsg in primary_msg.get_payload():
epochtime = email.utils.mktime_tz(email.utils.parsedate_tz(submsg["Date"])) if submsg["Date"] and email.utils.parsedate_tz(submsg["Date"])!=None else 0
if submsg.get_content_type()=="message/rfc822":
nsync.cache["DRAFTS"].append((epochtime,{ None: submsg }))
#Sort DRAFTS list in cache
nsync.cache["DRAFTS"].sort(key=lambda k: k[0],reverse=True)
#Okay, done with that. Update browser.
ui.main_browser.clear()
for entry_ in nsync.cache["DRAFTS"]:
entry = entry_[1][None]
ui.main_browser.add("@."+entry.get("Subject","")+"\t"+get_prettyprintable_column_str(entry)+"\t@."+entry.get("Date",""))
if ui.mb_selected<=len(nsync.cache["DRAFTS"]):
ui.main_browser.select(ui.mb_selected)
else:
ui.main_browser.deselect()
ui.mb_selected=0
print c_state.stack
return 1
##main_browser_callback: called when main_browser enter pressed
# add appropriate entries to stack, then call left_browser_callback
@staticmethod
def main_browser_callback():
ui.mb_selected=ui.main_browser.value()
if not ui.mb_selected:
return 1
if c_state.stack[-1][0]==ClientState.ATTACHMENTS: #attachments
attachment = nsync.cache["ATTACHMENTS"][ui.mb_selected-1][1][None]
c_state.stack.append((ClientState.SUBMESSAGE,attachment))
if not isinstance(attachment.get_payload(),str):
c_state.stack.append((ClientState.HEADERS,))
elif c_state.stack[-1][0]==ClientState.FOLDER: #folder
uidpath = c_state.stack[0][1]+"/"+nsync.cache[c_state.stack[0][1]][ui.mb_selected-1][1]["UID"]
try:
c_state.stack.append((ClientState.MESSAGE,email.parser.Parser().parse(open(os.path.join(cachedir,uidpath))),uidpath))
except IOError,OSError:
fl_alert("Message file does not exist on disk! Your cache may be out-of-date.")
return 1
c_state.stack.append((ClientState.HEADERS,))
elif c_state.stack[-1][0]==ClientState.RELATED: #RELATED view
uidpath = nsync.cache["RELATED"][ui.mb_selected-1][1]["FOLDER"]+"/"+nsync.cache["RELATED"][ui.mb_selected-1][1]["UID"]
try:
c_state.stack.append((ClientState.MESSAGE,email.parser.Parser().parse(open(os.path.join(cachedir,uidpath))),uidpath))
except IOError,OSError:
fl_alert("Message file does not exist on disk! Your cache may be out-of-date.")
return 1
c_state.stack.append((ClientState.HEADERS,))
elif c_state.stack[-1][0]==ClientState.DRAFTS: #DRAFTS view
c_state.stack.append((ClientState.SUBMESSAGE,nsync.cache["DRAFTS"][ui.mb_selected-1][1][None]))
c_state.stack.append((ClientState.HEADERS,))
ui.left_browser_callback(ui.left_browser,ClientUI.STACK_PUSHED)
return 1
##Turns cut object in browser gray
def show_cut_highlighted(self):
pass
def make_window(self):
#Create main window
self.window = Fl_Double_Window(516, 221, 920, 675, "MailTask Alpha")
self.window.resizable(self.window)
self.left_browser = Fl_Select_Browser(0, 0, 165, 675)
self.left_browser.type(2) #undocumented (wtf); makes selection highlight stick
self.left_browser.when(FL_WHEN_CHANGED)
self.left_browser.callback(lambda x: ClientUI.left_browser_callback(x,ClientUI.UI_INDUCED))
self.left_browser.end()
#Store the currently selected line in the left browser
self.lb_selected = 1
#Store the currently selected line in the main browser
self.mb_selected = 0
#Status Monitors
self.boxes = []
POSITIONS = [(250,0,70,25),(320,0,70,25),(390,0,70,25),(460,0,70,25),(530,0,70,25),(600,0,70,25),(670,0,70,25),(740,0,70,25),(810,0,70,25),
(250,25,70,25),(320,25,70,25),(390,25,70,25),(460,25,70,25),(530,25,70,25),(600,25,70,25),(670,25,70,25),(740,25,70,25),(810,25,70,25)]
for num in range(len(account_info)):
box = Fl_Box(*POSITIONS[num])
box.box(FL_EMBOSSED_BOX)
box.label(repr(num))
box.labelsize(12)
self.boxes.append(box)
#Buttons
# self.button1 = Fl_Button(250,0,70,25,"Toggle Editor")
# self.button1.callback(lambda ignored: c_state.toggle_editor())
# self.button1.labelsize(12)
# self.button2 = Fl_Button(320,0,70,25,"Cut Obj")
# self.button2.callback(lambda ignored: c_state.cut_obj())
# self.button2.labelsize(12)
# self.button3 = Fl_Button(390,0,70,25,"Copy Obj")
# self.button3.callback(lambda ignored: c_state.copy_obj())
# self.button3.labelsize(12)
# self.button4 = Fl_Button(460,0,70,25,"Paste Obj")
# self.button4.callback(lambda ignored: c_state.paste_obj())
# self.button4.labelsize(12)
# self.button5 = Fl_Button(530,0,70,25,"Delete Obj")
# self.button5.callback(lambda ignored: c_state.delete_obj())
# self.button5.labelsize(12)
# self.button6 = Fl_Button(600,0,70,25,"Back")
# self.button6.callback(lambda ignored: c_state.pop_view())
# self.button6.labelsize(12)
# self.button7 = Fl_Button(670,0,70,25,"Send Email")
# self.button7.callback(lambda ignored: c_state.send_email())
# self.button7.labelsize(12)
# self.button8 = Fl_Button(740,0,70,25,"Send All")
# self.button8.callback(lambda ignored: c_state.send_task())
# self.button8.labelsize(12)
# self.button9 = Fl_Button(810,0,70,25,"Reply All")
# self.button9.callback(lambda ignored: c_state.make_reply_all())
# self.button9.labelsize(12)
# self.button10 = Fl_Button(250,25,70,25,"Reply Sender")
# self.button10.callback(lambda ignored: c_state.make_reply_sender())
# self.button10.labelsize(12)
# self.button11 = Fl_Button(320,25,70,25,"Attach")
# self.button11.callback(lambda ignored: c_state.load_file_to_clipboard())
# self.button11.labelsize(12)
# self.button12 = Fl_Button(390,25,70,25,"Download")
# self.button12.callback(lambda ignored: c_state.download_attachment())
# self.button12.labelsize(12)
# self.button13 = Fl_Button(460,25,70,25,"New Task")
# self.button13.callback(lambda ignored: c_state.new_task())
# self.button13.labelsize(12)
# self.button14 = Fl_Button(530,25,70,25,"Show Done")
# self.button14.callback(lambda ignored: c_state.toggle_completed_task_visibility())
# self.button14.labelsize(12)
# self.button15 = Fl_Button(600,25,70,25,"Mark Completed")
# self.button15.callback(lambda ignored: c_state.toggle_current_task_completion())
# self.button15.labelsize(12)
# self.button16 = Fl_Button(670,25,70,25,"Rename")
# self.button16.callback(lambda ignored: c_state.rename_attachment())
# self.button16.labelsize(12)
# self.button17 = Fl_Button(740,25,70,25,"Addrbook")
# self.button17.callback(lambda ignored: c_state.update_addr_book_ui())
# self.button17.labelsize(12)
# self.button18 = Fl_Button(810,25,70,25,"Task To Top")
# self.button18.callback(lambda ignored: c_state.nowify())
# self.button18.labelsize(12)
self.upper_text_display = Fl_Box(165, 0, 755, 40)
self.upper_text_display.align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE)
self.lower_text_display1 = Fl_Box(165, 40, 350, 40)
self.lower_text_display1.box(FL_NO_BOX)
self.lower_text_display1.align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE)
self.lower_text_display2 = Fl_Box(515, 40, 250, 40)
self.lower_text_display2.box(FL_NO_BOX)
self.lower_text_display2.align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE)
self.lower_text_display3 = Fl_Box(765, 40, 155, 40)
self.lower_text_display3.box(FL_NO_BOX)
self.lower_text_display3.align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE)
self.main_editor = Fl_Text_Editor(165, 80, 755, 595)
self.main_buffer = Fl_Text_Buffer()
self.main_editor.buffer(self.main_buffer)
self.main_editor.wrap_mode(3,0)
self.main_editor.hide()
self.main_editor.end()
self.main_display = Fl_Text_Display(165,80,755,595)
self.main_display.buffer(self.main_buffer)
self.main_display.wrap_mode(3,0)
self.main_editor.hide()
#Whether main_editor or main_browser is visible depends on current mode
#Custom browser class, so we can override keyboard handler
#FLTK's documentation says a callback with FL_WHEN_ENTER_KEY_ALWAYS
#should do the equivalent of this, but it doesn't, so we need to do
#this instead. FLTK's abysmal documentation strikes again.
FLTK_Advanced_Parent = FLTK_Advanced_Browser(Fl_Select_Browser)
class Custom_Main_Browser(FLTK_Advanced_Parent):
def handle(self,etype):
#Is this the correct event to be handling this? If not, return 0
if etype!=FL_KEYDOWN or Fl_event_key()!=FL_Enter or Fl_event_state(FL_CTRL):
return FLTK_Advanced_Parent.handle(self,etype)
return ClientUI.main_browser_callback()
self.main_browser = Custom_Main_Browser(165,80,755,595)
self.main_browser.type(2) #stupid undocumented crap (see above)
self.main_browser.column_widths((350,250,155))
self.main_browser.hide()
self.window.end()
self.window.show()
#Global shortcut handler
Fl_add_handler(ClientState.shortcut_handler)
#Blocking dialog box
self.block_w = Fl_Single_Window(500,50)
self.block_w.label("Please wait")
self.block_w.set_modal()
self.block_w.callback(lambda x: 1)
self.boxblock = Fl_Box(0,0,500,50,"Server synchronization in progress.")
self.boxblock.align(FL_ALIGN_INSIDE|FL_ALIGN_CENTER)
self.block_w.end()
class ClientState:
#Enumeration of possible stack type tags
FOLDER=0
MESSAGE=1
HEADERS=2
ATTACHMENTS=3
SUBMESSAGE=4
RELATED=5
DRAFTS=6
##Class to use as type for clipboard
# Type of value is true string when is_uidpath is false
class Clipboard:
UIDPATH=0
TRUESTRING=1
SUBMESSAGE=2
TASKPATH=3
ATTACHMENTS=4
def __init__(self,typ,val):
self.type = typ
self.value = val
##Initialize ClientState object
def __init__(self):
#Current view stack
self.stack = []
#Rate limit address book updates
self.addrbook_rate_limit = 0
#Do we show completed tasks in task view
self.show_completed_tasks = False
#Internal clipboard objects
self.clipboard = ClientState.Clipboard(ClientState.Clipboard.TRUESTRING,"") #Contents of clipboard stored as MIME-encoded string
self.deathslayer = lambda: None #callable that removes cut item; called upon paste
#self.last_action_paste_from_copy = False #why is this here?
##Internal utility method to get single component of bottom-most message on the stack
def get_stacktop_internal(self,component):
for i in range(-1,-len(self.stack)-1,-1):
if self.stack[i][0]==ClientState.MESSAGE:
return self.stack[i][component]
return None
##Utility method to handle get Message object of current top of stack
# Specifically, the returned Message object should contain the entire
# message that needs to be uploaded to the path from
# get_stacktop_uidpath below.
def get_stacktop_msg(self):
return self.get_stacktop_internal(1)
##Utility method to handle getting UIDpath of current top of stack
def get_stacktop_uidpath(self):
return self.get_stacktop_internal(2)
#Methods to handle shortcuts
##Handle editor state toggle
def toggle_editor(self):
if ui.main_editor.visible():
ui.main_editor.hide()
ui.main_display.show()
if self.stack[-1][0]==ClientState.HEADERS:
#Remove blank lines so parser won't screw up.
headerlines = ui.main_buffer.text().splitlines()
headerlines[:] = (line for line in headerlines if line.strip()!="")
#Parse user's input
n_message = email.parser.HeaderParser().parsestr("\n".join(headerlines))
for header in MODIFIABLE_HEADERS:
if n_message[header]!=None:
if header in EMAIL_MODIFIABLE_HEADERS:
replacement_header=""
for entry in n_message[header].split(","):
if replacement_header!="":
replacement_header+=","
replacement_header+=addrbook_lookup(entry)
if header in self.stack[-2][1]:
self.stack[-2][1].replace_header(header,replacement_header)
else:
self.stack[-2][1][header]=replacement_header
else:
del self.stack[-2][1][header]
self.stack[-2][1][header]=n_message[header]
else: #delete missing header
del self.stack[-2][1][header]
else:
self.stack[-1][1].set_payload(ui.main_buffer.text())
#Send updated file to server
nsync.node_update(self.get_stacktop_uidpath(),self.get_stacktop_msg().as_string())
else:
if self.stack[-1][0]==ClientState.FOLDER and self.stack[0][1].find("Tasks")!=0 or self.stack[-1][0]!=ClientState.FOLDER and self.get_stacktop_uidpath().find("Tasks")!=0:
response = fl_ask("Warning: You are about to modify an IMAP message. Are you sure you want to do this?")
if not response:
return 1
self.oldtext = ui.main_buffer.text()
ui.main_display.hide()
if self.stack[-1][0]==ClientState.HEADERS:
#Buffer's text must use address book keys for modifiable headers
header_dict = CaseInsensitiveDict(self.stack[-2][1].items())
for header in MODIFIABLE_HEADERS:
if header in EMAIL_MODIFIABLE_HEADERS and header in header_dict:
header_dict[header] = addrbook_reverse_lookup(header_dict[header])
#Now, we need to recreate the message's tuple list
current_headers=CaseInsensitiveList()
for header in header_dict:
current_headers.append((header,header_dict[header]))
ui.main_buffer.text(get_editor_str(current_headers))
ui.main_editor.show()
else:
ui.main_editor.show()
return 1
# def cancel_editor(self):
# if not ui.main_editor.visible():
# return 0
# ui.main_buffer.text(self.oldtext)
# ui.main_editor.hide()
# ui.main_display.show()
# return 1
def cut_obj(self):
if Fl_Widget.visible(ui.main_browser) and not ui.main_browser.value():
return 0
if ui.main_editor.visible():
return 0
#If we're not ultimately cutting a task, or from a task, error out
if self.get_stacktop_uidpath()==None or self.get_stacktop_uidpath().find("Tasks")!=0:
fl_alert("Can't cut from non-Task object.")
return 1
c_index = ui.main_browser.value() #1-based!
if not ui.main_browser.value():
fl_alert("Nothing selected.")
return 1
if self.stack[-1][0]==ClientState.DRAFTS:
self.clipboard = ClientState.Clipboard(ClientState.Clipboard.SUBMESSAGE,nsync.cache["DRAFTS"][c_index-1][1][None])
slayeruidpath = self.get_stacktop_uidpath()
stacktopmsg = self.get_stacktop_msg()
child = nsync.cache["DRAFTS"][c_index-1][1][None]
def delcomponent():
mt_utils.delete_payload_component(stacktopmsg,child)
nsync.node_update(slayeruidpath,stacktopmsg.as_string())
self.deathslayer = delcomponent
elif self.stack[-1][0]==ClientState.RELATED:
selected_message_headers = nsync.cache["RELATED"][c_index-1][1]
related_msg_uidpath = selected_message_headers["FOLDER"]+"/"+selected_message_headers["UID"]
self.clipboard = ClientState.Clipboard(ClientState.Clipboard.UIDPATH,related_msg_uidpath)
slayer_uidpath = self.get_stacktop_uidpath()
slayer_origmsg = self.get_stacktop_msg()
slayer_cutrelatedmid = mt_utils.get_message_id(selected_message_headers,selected_message_headers["FOLDER"])
def slay_from_references():
newreferencelist = mt_utils.get_related_ids(slayer_origmsg)
index = gfind(newreferencelist,slayer_cutrelatedmid)
if index==-1:
fl_alert("Warning: failed to delete cut message ID from related list of original message.")