enabling debug output
[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         proto.getCapabilities(
204                         ).addCallback(self.cbCapabilities, proto
205                         ).addErrback(self.ebCapabilities, proto
206                         )
207
208     def cbCapabilities(self,reason,proto):
209         print "#"*30
210         print "# If you have problems to log into your imap-server, please send me the output of the following line"
211         print "# cbCapabilities",reason
212         print "#"*30
213         self.doLogin(proto)
214
215     def ebCapabilities(reason,proto):
216         print "ebCapabilities",reason
217         
218     def onConnectFailed(self, reason):
219         self["infolabel"].setText(reason.getErrorMessage())
220
221     def onAuthentication(self, result, proto):
222         self.proto = proto
223         self["infolabel"].setText("logged in")
224         proto.list("", "*"
225         ).addCallback(self.onMailboxList, proto
226         )
227         
228     def doLogin(self, proto):
229         print "login secure"
230         useTLS = False #True
231         if useTLS:
232             context = proto.context.getContext()
233             d = proto.startTLS(context)
234             d = d.addCallback(proto.authenticate, config.plugins.emailimap.password.value)
235         else:
236             d = proto.authenticate(config.plugins.emailimap.password.value)
237         d.addCallback(self.onAuthentication, proto)
238         d.addErrback(self.onAuthenticationFailed, proto)
239         return d
240
241     def onAuthenticationFailed(self, failure, proto):
242         # If it failed because no SASL mechanisms match
243         print "onAuthenticationFailed", failure, proto
244         self["infolabel"].setText(failure.getErrorMessage())
245         try:
246             failure.trap(imap4.NoSupportedAuthentication)
247             self.doLoginInsecure(proto)
248         except Exception,e:
249             print e,e.message
250             
251     def doLoginInsecure(self, proto):
252         print "login INSECURE"
253         proto.login(config.plugins.emailimap.username.value, config.plugins.emailimap.password.value
254                 ).addCallback(self.onAuthentication, proto
255                 ).addErrback(self.onInsecureAuthenticationFailed, proto
256                 )
257     
258     def onInsecureAuthenticationFailed(self, failure, proto):
259         print "onInsecureAuthenticationFailed", failure, proto
260         self["infolabel"].setText(failure.getErrorMessage())
261         
262     def onMailboxList(self, result, proto):
263         print "onMailboxList", result, proto
264         list = []
265         for i in result:
266             flags, hierarchy_delimiter, name = i
267             list.append((UTF7toUTF8(name).encode('utf-8'), i))
268         self["boxlist"].l.setList(list) 
269         
270     def onExamine(self, result, mboxname, proto):
271         print "onExamine", result, mboxname
272         self.setTitle("Mailbox: "+mboxname)
273         self.currentmailbox = mboxname
274         numMessagesinFolder = int(result['EXISTS'])
275         if numMessagesinFolder <= 0:
276             self["infolabel"].setText("Box '"+mboxname+"' is empty")
277             self["messagelist"].l.setList([]) 
278         
279         else:
280             if config.plugins.emailimap.maxheadertoload.value > 0:
281                 maxMessagesToFetch = config.plugins.emailimap.maxheadertoload.value
282                 startmsg = numMessagesinFolder-maxMessagesToFetch+1
283                 if startmsg <= 0:
284                     startmsg = 1
285                 rangeToFetch = [startmsg, numMessagesinFolder]
286             else:
287                 rangeToFetch = [1, numMessagesinFolder]
288             self["infolabel"].setText("loading headers "+str(rangeToFetch[0])+"-"+str(rangeToFetch[1])+" of Box '"+mboxname+"'")
289             
290             try:
291 #                proto.fetchEnvelope('%i:%i'%(rangeToFetch[0], rangeToFetch[1])    #'1:*'
292 #                           ).addCallback(self.onEnvelopeList, proto
293 #                           )
294                 proto.fetchHeaders('%i:%i'%(rangeToFetch[0], rangeToFetch[1])    #'1:*'
295                            ).addCallback(self.onHeaderList, proto
296                            )
297
298             except imap4.IllegalServerResponse, e:
299                 print e
300                 
301     def onExamineFailed(self, failure, proto):
302         print "onExamineFailed", failure, proto
303         self["infolabel"].setText(failure.getErrorMessage())
304
305     def onHeaderList(self, result, proto):
306         print "onHeaderList"#,result,proto
307         self["infolabel"].setText("headers loaded, now parsing ...")
308         list = []
309         for m in result:
310             try:
311                 list.append(self.buildMessageListItem(MessageHeader(m, result[m]['RFC822.HEADER'])))
312             except Exception,e:
313                 print e
314         list.reverse()
315         self["messagelist"].l.setList(list) 
316         self["infolabel"].setText("have "+str(len(result))+" messages ")
317             
318     def buildMessageListItem(self, message):
319         res = [ message ]
320         res.append(MultiContentEntryText(pos=(5, 0), size=(380, 19), font=0, text=message.getSenderString()))
321         res.append(MultiContentEntryText(pos=(5, 19), size=(380, 19), font=0, text=message.get('date', default='kein Datum')))
322         res.append(MultiContentEntryText(pos=(5, 38), size=(380, 19), font=0, text=message.getSubject()))
323         return res  
324     #
325     # IMailboxListener methods
326     #
327     def modeChanged(self, writeable):
328         print "modeChanged", writeable
329
330     def flagsChanged(self, newFlags):
331         print "flagsChanged", newFlags
332
333     def newMessages(self, exists, recent):
334         print "newMessages", exists, recent
335       
336 class ScreenMailView(Screen):
337     skin=""
338     def __init__(self, session, email, args = 0):
339         self.session = session
340         self.email = email
341         self.skin = "<screen position=\"85,80\" size=\"550,476\" title=\"view Email\" >"
342         self.skin +=  """<widget name="from" position="0,0" size="550,25"  font="Regular;20" />
343             <widget name="date" position="0,25" size="550,25"  font="Regular;20" />
344             <widget name="subject" position="0,50" size="550,25"  font="Regular;20" />
345             <widget name="body" position="0,75" size="550,375"  font="Regular;20" />
346             <widget name="buttonred" position="10,436" size="100,30" backgroundColor="red" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/> 
347             <widget name="buttongreen" position="120,436" size="100,30" backgroundColor="green" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/> 
348             <widget name="buttonyellow" position="230,436" size="100,30" backgroundColor="yellow" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/> 
349             <widget name="buttonblue" position="340,436" size="100,30" backgroundColor="blue" valign="center" halign="center" zPosition="2"  foregroundColor="white" font="Regular;16"/>            
350         </screen>""" 
351         Screen.__init__(self, session)
352         self["from"] = Label(_("From: %s" %self.email.get('from', 'no-from')))
353         self["date"] = Label(_("Date: %s" %self.email.get('date', 'no-date')))
354         self["subject"] = Label(_(self.email.get('subject', 'no-subject')))
355         self["body"] = ScrollLabel(_(self.email.messagebodys[0].getData()))
356         self["buttonred"] = Label("")
357         self["buttongreen"] = Label("")
358         self["buttonyellow"] =  Label("Headers")
359         self["buttonblue"] =  Label(_("delete"))
360         self["actions"] = ActionMap(["WizardActions", "DirectionActions", "MenuActions", "ShortcutActions"], 
361             {
362              "back": self.close, 
363              "up": self["body"].pageUp, 
364              "down": self["body"].pageDown, 
365              "left": self["body"].pageUp, 
366              "right": self["body"].pageDown, 
367              "red": self.selectBody, 
368              "green": self.selectAttachment, 
369              "yellow": self.openMessagesHeaders, 
370              "blue": self.delete, 
371              
372              }, -1)
373         self.onLayoutFinish.append(self.updateButtons)
374         
375     def delete(self):
376         pass #self.session.openWithCallback(self.deleteCB, ChoiceBox, title="really delete Mail?", list=[(_("yes"), True),(_("no"), False)])
377
378     def deleteCB(self, returnValue):
379         if returnValue[1] is True:
380             pass
381         
382     def openMessagesHeaders(self):
383         pass #self.session.open(ScreenMailViewHeader,self.profil,self.email)
384                    
385     def updateButtons(self):
386         self["buttonred"].setText(_("Bodys"))
387         if len(self.email.attachments):
388             self["buttongreen"].setText("Attachments")
389         else:
390             self["buttongreen"].setText("")  
391     
392     def selectBody(self):
393         if len(self.email.messagebodys):
394             list = []
395             for a in self.email.messagebodys:
396                 list.append((a.getContenttype(), a))
397             self.session.openWithCallback(self.selectBodyCB, ChoiceBox, _("select Body"), list)
398             
399     def selectBodyCB(self, choice):
400         if choice is not None:
401            self["body"].setText(choice[1].getData())
402             
403     def selectAttachment(self):
404         if len(self.email.attachments):
405             list = []
406             for a in self.email.attachments:
407                 list.append((a.getFilename(), a))
408             self.session.openWithCallback(self.selectAttachmentCB, ChoiceBox, _("select Attachment"), list)
409             
410     def selectAttachmentCB(self, choice):
411         if choice is not None:
412             print "Attachment selected", choice[1].getFilename()
413             #showMessageBox(self.session)
414     
415 class MailList(MenuList, HTMLComponent, GUIComponent):
416     def __init__(self, list):
417         MenuList.__init__(self, list)
418         GUIComponent.__init__(self)
419         self.l = eListboxPythonMultiContent()
420         self.list = list
421         self.l.setList(list)
422         self.l.setFont(0, gFont("Regular", 18))
423         self.l.setFont(1, gFont("Regular", 20))
424         
425     GUI_WIDGET = eListbox
426
427     def postWidgetCreate(self, instance):
428         instance.setContent(self.l)
429         instance.setItemHeight(60)
430
431 class MessageHeader(object):        
432     def __init__(self, uid, message):
433         self.uid = uid #must be int
434         self.message = email.Parser.Parser().parsestr(message)
435         
436     def getSenderString(self):
437         if self.get("from") is None:
438             return "no sender"
439         else:
440             return self.get("from")
441         
442     def getSubject(self):
443         if self.get("subject") is None:
444             return "no subject"
445             """
446         # need some decoding for subjects like =?iso-8859-1?Q?=DCbermut?=
447         elif self.get("subject").startswith("=?") and self.get("subject").endswith("?="):
448             c = self.get("subject").split("?")
449             print c
450             codec = c[1]
451             text = "?".join(c[3:])
452             
453             
454             print "CODEC",codec
455             print "TEXT",text
456             return text.decode(codec).encode("utf-8")
457             """
458         else:
459             return self.get("subject")
460             
461     def get(self, key, default=None):
462         return self.message.get(key,failobj=default)    
463         
464     def __str__(self):
465         return "<MessageHeader uid="+str(self.uid)+", subject="+self.get("subject","no-subject")+">"
466     
467 ############
468 class EmailBody:
469     def __init__(self,data):
470         self.data = data
471     
472     def getEncoding(self):
473         return self.data.get_content_charset()
474     
475     def getData(self):
476         text = self.data.get_payload(decode=True)
477         if self.getEncoding() is not None:
478             print "decoding text with charset ",self.getEncoding()
479             text = text.decode(self.getEncoding())            
480         if self.getContenttype() == "text/html":
481             print "stripping html"
482             text = TagStrip().strip_readable(text)
483         try:
484             return text.encode('utf-8')
485         except Exception,e:
486             print e
487             return text
488     
489     def getContenttype(self):
490         return self.data.get_content_type()
491
492 ############
493 class EmailAttachment:
494     def __init__(self, filename, contenttype, data):
495         self.filename = filename
496         self.contenttype = contenttype
497         self.data = data
498         
499     def save(self,folder):
500         try:
501             fp = open(folder+"/"+self.getFilename(),"wb")
502             fp.write(self.data)
503             fp.close()
504         except Exception,e:
505             print e
506             return False
507         return True
508             
509     def getFilename(self):
510         return self.filename 
511     
512     def getContenttype(self):
513         return self.contenttype 
514     
515     def getData(self):
516         return self.data
517     
518 def UTF7toUTF8(str):
519     return imap4.decoder(str)[0]
520
521 def UTF8toUTF7(str):
522     return imap4.encoder(str.decode('utf-8'))[0]
523
524 def main(session, **kwargs):
525     session.open(EmailScreen)    
526
527 def Plugins(path, **kwargs):
528     global plugin_path
529     plugin_path = path
530     return [
531              PluginDescriptor(name="Email Client", description="view Emails via IMAP4", 
532              where = PluginDescriptor.WHERE_PLUGINMENU, 
533              fnc = main,
534              icon="plugin.png"
535              ), 
536              #PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = startScrobbler)
537         ]