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