some working on encodings
[enigma2-plugins.git] / emailclient / src / plugin.py
1 from Components.ActionMap import ActionMap
2 from Components.GUIComponent import GUIComponent
3 from Components.HTMLComponent import HTMLComponent
4 from Components.Label import Label
5 from Components.MenuList import MenuList
6 from Components.MultiContent import MultiContentEntryText
7 from Components.Pixmap import Pixmap, MovingPixmap
8 from Components.ScrollLabel import ScrollLabel
9 from Components.config import config, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText
10 from EmailConfig import EmailConfigScreen
11 from Plugins.Plugin import PluginDescriptor
12 from Screens.ChoiceBox import ChoiceBox
13 from Screens.Screen import Screen
14 from enigma import eListboxPythonMultiContent, eListbox, gFont
15 from twisted.mail import imap4
16 from zope.interface import implements
17 import email
18 import email.Parser
19 from TagStrip import TagStrip
20 import protocol
21 import uu
22
23
24
25
26
27 config.plugins.emailimap = ConfigSubsection()
28 config.plugins.emailimap.username = ConfigText("user", fixed_size=False)
29 config.plugins.emailimap.password = ConfigText("password", fixed_size=False)
30 config.plugins.emailimap.server = ConfigText("please.config.first", fixed_size=False)
31 config.plugins.emailimap.port = ConfigInteger(143, limits = (1, 65536))
32
33 # 0= fetch all header , 10= fetch only the last 10 headers/messages of a mailbox
34 config.plugins.emailimap.maxheadertoload = ConfigInteger(0, limits = (1, 100)) 
35
36 class EmailHandler:
37     def __init__(self):
38         pass
39     def onConnect(self, proto):
40         pass
41
42 class EmailScreen(Screen, EmailHandler):
43     implements(imap4.IMailboxListener)
44     
45     skin = """
46         <screen position="110,83" size="530,430" title="Email" >
47             <widget name="boxlist" position="0,0" size="150,400" scrollbarMode="showOnDemand" />            
48             <widget name="messagelist" position="150,0" size="380,400" scrollbarMode="showOnDemand" />            
49             <widget name="infolabel" position="0,400" size="530,30"   foregroundColor=\"white\" font=\"Regular;18\" />           
50         </screen>"""
51          
52     currentmailbox = None
53     proto = None
54     
55     def __init__(self, session, args = 0):
56         EmailHandler.__init__(self)
57         self.session = session
58         
59         self.skin = EmailScreen.skin
60         Screen.__init__(self, session)
61         protocol.createFactory(self, config.plugins.emailimap.username.value, config.plugins.emailimap.server.value, config.plugins.emailimap.port.value)
62
63         self["actions"] = ActionMap(["InfobarChannelSelection", "WizardActions", "DirectionActions", "MenuActions", "ShortcutActions", "GlobalActions", "HelpActions", "NumberActions"], 
64             {
65              "ok": self.action_ok, 
66              "back": self.action_exit, 
67              "historyNext": self.selectMessagelist, 
68              "historyBack": self.selectBoxlist, 
69              "down":        self.down, 
70              "up":          self.up, 
71              "left":        self.left, 
72              "right":       self.right, 
73              "menu":        self.action_menu, 
74              }, -1)
75         self["boxlist"] = MenuList([])
76         self["messagelist"] = MailList([])
77         self["infolabel"] = Label("")
78         self.onLayoutFinish.append(self.selectBoxlist)
79         
80     def action_menu(self):
81         self.session.open(EmailConfigScreen)
82         
83     def selectBoxlist(self):
84         self.currList = "boxlist"
85         self["boxlist"].selectionEnabled(1)
86         self["messagelist"].selectionEnabled(0)
87         
88     def selectMessagelist(self):
89         self.currList = "messagelist"
90         self["boxlist"].selectionEnabled(0)
91         self["messagelist"].selectionEnabled(1)
92     
93     def up(self):
94         self[self.currList].up()
95     
96     def down(self):
97         self[self.currList].down()
98         
99     def left(self):
100         self[self.currList].pageUp()
101     
102     def right(self):
103         self[self.currList].pageDown()
104     
105     def action_ok(self):
106         if self.currList == "boxlist":
107             self.onBoxSelected()
108         else:
109             self.onMessageSelected()
110         
111     def onBoxSelected(self):
112         c = self["boxlist"].getCurrent()
113         if c is not None:
114             self.proto.examine(UTF7toUTF8(c[1][2])
115                                ).addCallback(self.onExamine, c[0] , self.proto
116                               ).addErrback(self.onExamineFailed, self.proto
117                               )
118     
119     def onMessageSelected(self):
120         c = self["messagelist"].getCurrent()
121         if c is not None:
122             self.fetchMessageSize(c[0])
123             
124     def fetchMessageSize(self, message):
125         print "fetchMessageSize",message
126         self.proto.fetchSize(message.uid
127             ).addCallback(self.onMessageSizeLoaded, message, self.proto
128             ).addErrback(self.onMessageLoadFailed, message, self.proto
129             )
130             
131     def onMessageSizeLoaded(self, result, message, proto):
132         print "onMessageSizeLoaded", result, message
133         size = int(result[message.uid]['RFC822.SIZE'])
134         self.MAX_MESSAGE_SIZE_TO_OPEN = 4000000
135         if size >= self.MAX_MESSAGE_SIZE_TO_OPEN:
136             #ask here to open message
137             print "message to large to open (size=", size, ")"
138         else:
139             self.loadMessage(message)
140         
141 #    def fetchBodyStructure(self, message):
142 #        print "fetchBodyStructure",message
143 #        self.proto.fetchBodyStructure(message.uid
144 #            ).addCallback(self.onBodystructureLoaded, message, self.proto
145 #            ).addErrback(self.onMessageLoadFailed, message, self.proto
146 #            )
147     
148     def loadMessage(self, message):
149         print "loadMessage",message
150         self["infolabel"].setText("loading message")
151         
152         self.proto.fetchMessage(message.uid
153             ).addCallback(self.onMessageLoaded, message, self.proto
154             ).addErrback(self.onMessageLoadFailed, message, self.proto
155             )
156     
157     def onMessageLoaded(self, result, message, proto):
158         self["infolabel"].setText("parsing message")
159         print "onMessageLoaded"#,result,message
160         msgstr = result[message.uid]['RFC822']
161         msg = email.Parser.Parser().parsestr(msgstr)
162         msg.messagebodys = []
163         msg.attachments = []
164         
165         if msg.is_multipart():
166             for part in msg.walk():
167                 if part.get_content_maintype()=="multipart":
168                     continue
169                 if part.get_content_maintype() == 'text' and part.get_filename() is None:
170                     if part.get_content_subtype() == "html":
171                         msg.messagebodys.append(EmailBody(part))
172                     elif part.get_content_subtype() == "plain":
173                         msg.messagebodys.append(EmailBody(part))
174                     else:
175                         print "unkown content type= ", part.get_content_maintype(), "/", part.get_content_subtype()
176                 else:
177                      print "found Attachment with  ", part.get_content_type(), "and name", part.get_filename()
178                      msg.attachments.append(EmailAttachment(part.get_filename(), part.get_content_type(), part.get_payload()))    
179         else:
180             msg.messagebodys.append(EmailBody(msg))
181         self.session.open(ScreenMailView, msg)
182             
183     def onMessageLoadFailed(self, failure, message, proto):
184         print "onMessageLoadFailed", failure, message
185         self["infolabel"].setText(failure.getErrorMessage())
186
187     def action_exit(self):
188         if self.proto is not None:
189             self.proto.logout(
190                             ).addCallback(self.onLogedOut, self.proto
191                 ).addErrback(self.onLogedOut, self.proto
192                             )
193         else:
194             self.close()
195     
196     def onLogedOut(self, result, proto):
197         print "onLogedOut", result
198         self.close()
199         
200             
201     def onConnect(self, proto):
202         self["infolabel"].setText("connected")
203         self.doLogin(proto)
204         #self.doLoginInsecure(proto)
205         
206     def onConnectFailed(self, reason):
207         self["infolabel"].setText(reason.getErrorMessage())
208
209     def onAuthentication(self, result, proto):
210         self.proto = proto
211         self["infolabel"].setText("logged in")
212         proto.list("", "*"
213         ).addCallback(self.onMailboxList, proto
214         )
215         
216     def doLogin(self, proto):
217         print "login secure"
218         proto.authenticate(config.plugins.emailimap.password.value
219                            ).addCallback(self.onAuthentication, proto
220                            ).addErrback(self.onAuthenticationFailed, proto
221                            )
222
223     def onAuthenticationFailed(self, failure, proto):
224         # If it failed because no SASL mechanisms match
225         print "onAuthenticationFailed", failure, proto
226         self["infolabel"].setText(failure.getErrorMessage())
227         try:
228             failure.trap(imap4.NoSupportedAuthentication)
229             self.doLoginInsecure(proto)
230         except Exception,e:
231             print e,e.message
232             
233     def doLoginInsecure(self, proto):
234         print "login INSECURE"
235         proto.login(config.plugins.emailimap.username.value, config.plugins.emailimap.password.value
236                 ).addCallback(self.onAuthentication, proto
237                 ).addErrback(self.onInsecureAuthenticationFailed, proto
238                 )
239     
240     def onInsecureAuthenticationFailed(self, failure, proto):
241         print "onInsecureAuthenticationFailed", failure, proto
242         self["infolabel"].setText(failure.getErrorMessage())
243         
244     def onMailboxList(self, result, proto):
245         print "onMailboxList", result, proto
246         list = []
247         for i in result:
248             flags, hierarchy_delimiter, name = i
249             list.append((UTF7toUTF8(name).encode('utf-8'), i))
250         self["boxlist"].l.setList(list) 
251         
252     def onExamine(self, result, mboxname, proto):
253         print "onExamine", result, mboxname
254         self.setTitle("Mailbox: "+mboxname)
255         self.currentmailbox = mboxname
256         numMessagesinFolder = int(result['EXISTS'])
257         if numMessagesinFolder <= 0:
258             self["infolabel"].setText("Box '"+mboxname+"' is empty")
259             self["messagelist"].l.setList([]) 
260         
261         else:
262             if config.plugins.emailimap.maxheadertoload.value > 0:
263                 maxMessagesToFetch = config.plugins.emailimap.maxheadertoload.value
264                 startmsg = numMessagesinFolder-maxMessagesToFetch+1
265                 if startmsg <= 0:
266                     startmsg = 1
267                 rangeToFetch = [startmsg, numMessagesinFolder]
268             else:
269                 rangeToFetch = [1, numMessagesinFolder]
270             self["infolabel"].setText("loading headers "+str(rangeToFetch[0])+"-"+str(rangeToFetch[1])+" of Box '"+mboxname+"'")
271             
272             try:
273 #                proto.fetchEnvelope('%i:%i'%(rangeToFetch[0], rangeToFetch[1])    #'1:*'
274 #                           ).addCallback(self.onEnvelopeList, proto
275 #                           )
276                 proto.fetchHeaders('%i:%i'%(rangeToFetch[0], rangeToFetch[1])    #'1:*'
277                            ).addCallback(self.onHeaderList, proto
278                            )
279
280             except imap4.IllegalServerResponse, e:
281                 print e
282                 
283     def onExamineFailed(self, failure, proto):
284         print "onExamineFailed", failure, proto
285         self["infolabel"].setText(failure.getErrorMessage())
286
287     def onHeaderList(self, result, proto):
288         print "onHeaderList"#,result,proto
289         self["infolabel"].setText("headers loaded, now parsing ...")
290         list = []
291         for m in result:
292             try:
293                 list.append(self.buildMessageListItem(MessageHeader(m, result[m]['RFC822.HEADER'])))
294             except Exception,e:
295                 print e
296         list.reverse()
297         self["messagelist"].l.setList(list) 
298         self["infolabel"].setText("have "+str(len(result))+" messages ")
299             
300     def buildMessageListItem(self, message):
301         res = [ message ]
302         res.append(MultiContentEntryText(pos=(5, 0), size=(380, 19), font=0, text=message.getSenderString()))
303         res.append(MultiContentEntryText(pos=(5, 19), size=(380, 19), font=0, text=message.get('date', default='kein Datum')))
304         res.append(MultiContentEntryText(pos=(5, 38), size=(380, 19), font=0, text=message.getSubject()))
305         return res  
306     #
307     # IMailboxListener methods
308     #
309     def modeChanged(self, writeable):
310         print "modeChanged", writeable
311
312     def flagsChanged(self, newFlags):
313         print "flagsChanged", newFlags
314
315     def newMessages(self, exists, recent):
316         print "newMessages", exists, recent
317       
318 class ScreenMailView(Screen):
319     skin=""
320     def __init__(self, session, email, args = 0):
321         self.session = session
322         self.email = email
323         self.skin = "<screen position=\"85,80\" size=\"550,476\" title=\"view Email\" >"
324         self.skin +=  """<widget name="from" position="0,0" size="550,25"  font="Regular;20" />
325             <widget name="date" position="0,25" size="550,25"  font="Regular;20" />
326             <widget name="subject" position="0,50" size="550,25"  font="Regular;20" />
327             <widget name="body" position="0,75" size="550,375"  font="Regular;20" />
328             <widget name="buttonred" position="10,436" size="100,30" backgroundColor="red" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/> 
329             <widget name="buttongreen" position="120,436" size="100,30" backgroundColor="green" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/> 
330             <widget name="buttonyellow" position="230,436" size="100,30" backgroundColor="yellow" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/> 
331             <widget name="buttonblue" position="340,436" size="100,30" backgroundColor="blue" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/>            
332         </screen>""" 
333         Screen.__init__(self, session)
334         self["from"] = Label(_("From: %s" %self.email.get('from', 'no-from')))
335         self["date"] = Label(_("Date: %s" %self.email.get('date', 'no-date')))
336         self["subject"] = Label(_(self.email.get('subject', 'no-subject')))
337         self["body"] = ScrollLabel(_(self.email.messagebodys[0].getData()))
338         self["buttonred"] = Label("")
339         self["buttongreen"] = Label("")
340         self["buttonyellow"] =  Label("Headers")
341         self["buttonblue"] =  Label(_("delete"))
342         self["actions"] = ActionMap(["WizardActions", "DirectionActions", "MenuActions", "ShortcutActions"], 
343             {
344              "back": self.close, 
345              "up": self["body"].pageUp, 
346              "down": self["body"].pageDown, 
347              "left": self["body"].pageUp, 
348              "right": self["body"].pageDown, 
349              "red": self.selectBody, 
350              "green": self.selectAttachment, 
351              "yellow": self.openMessagesHeaders, 
352              "blue": self.delete, 
353              
354              }, -1)
355         self.onLayoutFinish.append(self.updateButtons)
356         
357     def delete(self):
358         pass #self.session.openWithCallback(self.deleteCB, ChoiceBox, title="really delete Mail?", list=[(_("yes"), True),(_("no"), False)])
359
360     def deleteCB(self, returnValue):
361         if returnValue[1] is True:
362             pass
363         
364     def openMessagesHeaders(self):
365         pass #self.session.open(ScreenMailViewHeader,self.profil,self.email)
366                    
367     def updateButtons(self):
368         self["buttonred"].setText(_("Bodys"))
369         if len(self.email.attachments):
370             self["buttongreen"].setText("Attachments")
371         else:
372             self["buttongreen"].setText("")  
373     
374     def selectBody(self):
375         if len(self.email.messagebodys):
376             list = []
377             for a in self.email.messagebodys:
378                 list.append((a.getContenttype(), a))
379             self.session.openWithCallback(self.selectBodyCB, ChoiceBox, _("select Body"), list)
380             
381     def selectBodyCB(self, choice):
382         if choice is not None:
383            self["body"].setText(choice[1].getData())
384             
385     def selectAttachment(self):
386         if len(self.email.attachments):
387             list = []
388             for a in self.email.attachments:
389                 list.append((a.getFilename(), a))
390             self.session.openWithCallback(self.selectAttachmentCB, ChoiceBox, _("select Attachment"), list)
391             
392     def selectAttachmentCB(self, choice):
393         if choice is not None:
394             print "Attachment selected", choice[1].getFilename()
395             #showMessageBox(self.session)
396     
397 class MailList(MenuList, HTMLComponent, GUIComponent):
398     def __init__(self, list):
399         MenuList.__init__(self, list)
400         GUIComponent.__init__(self)
401         self.l = eListboxPythonMultiContent()
402         self.list = list
403         self.l.setList(list)
404         self.l.setFont(0, gFont("Regular", 18))
405         self.l.setFont(1, gFont("Regular", 20))
406         
407     GUI_WIDGET = eListbox
408
409     def postWidgetCreate(self, instance):
410         instance.setContent(self.l)
411         instance.setItemHeight(60)
412
413 class MessageHeader(object):        
414     def __init__(self, uid, message):
415         self.uid = uid #must be int
416         self.message = email.Parser.Parser().parsestr(message)
417         
418     def getSenderString(self):
419         if self.get("from") is None:
420             return "no sender"
421         else:
422             return self.get("from")
423         
424     def getSubject(self):
425         if self.get("subject") is None:
426             return "no subject"
427             """
428         # need some decoding for subjects like =?iso-8859-1?Q?=DCbermut?=
429         elif self.get("subject").startswith("=?") and self.get("subject").endswith("?="):
430             c = self.get("subject").split("?")
431             print c
432             codec = c[1]
433             text = "?".join(c[3:])
434             
435             
436             print "CODEC",codec
437             print "TEXT",text
438             return text.decode(codec).encode("utf-8")
439             """
440         else:
441             return self.get("subject")
442             
443     def get(self, key, default=None):
444         return self.message.get(key,failobj=default)    
445         
446     def __str__(self):
447         return "<MessageHeader uid="+str(self.uid)+", subject="+self.get("subject","no-subject")+">"
448     
449 ############
450 class EmailBody:
451     def __init__(self,data):
452         self.data = data
453     
454     def getEncoding(self):
455         return self.data.get_content_charset()
456     
457     def getData(self):
458         text = self.data.get_payload(decode=True)
459         if self.getEncoding() is not None:
460             print "decoding text with charset ",self.getEncoding()
461             text = text.decode(self.getEncoding())            
462         if self.getContenttype() == "text/html":
463             print "stripping html"
464             text = TagStrip().strip_readable(text)
465         try:
466             return text.encode('utf-8')
467         except Exception,e:
468             print e
469             return text
470     
471     def getContenttype(self):
472         return self.data.get_content_type()
473
474 ############
475 class EmailAttachment:
476     def __init__(self, filename, contenttype, data):
477         self.filename = filename
478         self.contenttype = contenttype
479         self.data = data
480         
481     def save(self,folder):
482         try:
483             fp = open(folder+"/"+self.getFilename(),"wb")
484             fp.write(self.data)
485             fp.close()
486         except Exception,e:
487             print e
488             return False
489         return True
490             
491     def getFilename(self):
492         return self.filename 
493     
494     def getContenttype(self):
495         return self.contenttype 
496     
497     def getData(self):
498         return self.data
499     
500 def UTF7toUTF8(str):
501     return imap4.decoder(str)[0]
502
503 def UTF8toUTF7(str):
504     return imap4.encoder(str.decode('utf-8'))[0]
505
506 def main(session, **kwargs):
507     session.open(EmailScreen)    
508
509 def Plugins(path, **kwargs):
510     global plugin_path
511     plugin_path = path
512     return [
513              PluginDescriptor(name="Email Client", description="view Emails via IMAP4", 
514              where = PluginDescriptor.WHERE_PLUGINMENU, 
515              fnc = main
516              ), 
517              #PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = startScrobbler)
518         ]