1 # -*- coding: utf-8 -*-
8 from Components.ActionMap import ActionMap
9 from Components.Label import Label
10 from Screens.MessageBox import MessageBox
11 from Components.MenuList import MenuList
12 from Components.MultiContent import MultiContentEntryText
13 from Components.ScrollLabel import ScrollLabel
14 from Components.Button import Button
15 from Components.config import config, ConfigSubsection, ConfigInteger, ConfigEnableDisable
16 from Plugins.Plugin import PluginDescriptor
17 from Screens.ChoiceBox import ChoiceBox
18 from Screens.Screen import Screen
19 from Tools import Notifications
20 from enigma import eListboxPythonMultiContent, gFont, eTimer #@UnresolvedImport # pylint: disable-msg=E0611
21 from twisted.mail import imap4 #@UnresolvedImport
22 from zope.interface import implements
24 from email.header import decode_header
26 from TagStrip import strip_readable
27 from protocol import createFactory
29 from . import _, initLog, debug, scaleH, scaleV, DESKTOP_WIDTH, DESKTOP_HEIGHT #@UnresolvedImport # pylint: disable-msg=F0401
30 mailAccounts = [] # contains all EmailAccount objects
31 from EmailConfig import EmailConfigOptions, EmailConfigAccount
33 config.plugins.emailimap = ConfigSubsection()
34 config.plugins.emailimap.showDeleted = ConfigEnableDisable(default=False)
35 config.plugins.emailimap.timeout = ConfigInteger(default=0, limits=(0, 90)) # in seconds
36 config.plugins.emailimap.verbose = ConfigEnableDisable(default=True)
37 config.plugins.emailimap.debug = ConfigEnableDisable(default=False)
39 def decodeHeader(text, default=''):
42 text = text.replace('\r',' ').replace('\n',' ').replace('\t',' ')
43 text = re.sub('\s\s+', ' ', text)
45 for part in decode_header(text):
46 (content, charset) = part
47 # print("decodeHeader content/charset: %s/%s" %(repr(content),charset))
49 textNew += content.decode(charset)
53 return textNew.encode('utf-8')
54 except UnicodeDecodeError: # for faulty mail software systems
55 return textNew.decode('iso-8859-1').encode('utf-8')
61 class EmailScreen(Screen):
63 This is the main screen for interacting with the user.
64 It contains the list of mailboxes (boxlist) on the left and
65 the list of messages (messagelist) on the right.
66 At the bottom we have a line for info messages.
67 It is specific for one account.
70 width = scaleH(-1, 530)
71 height = scaleV(-1, 430)
72 boxlistWidth = scaleH(-1, 150)
73 messagelistWidth = width-boxlistWidth
74 infolabelHeight = scaleV(-1, 30)
76 <screen position="%d,%d" size="%d,%d" title="Email" >
77 <widget name="boxlist" position="0,0" size="%d,%d" scrollbarMode="showOnDemand" />
78 <widget name="messagelist" position="%d,%d" size="%d,%d" scrollbarMode="showOnDemand" />
79 <widget name="infolabel" position="%d,%d" size="%d,%d" foregroundColor=\"white\" font=\"Regular;%d\" />
81 (DESKTOP_WIDTH-width)/2, (DESKTOP_HEIGHT-height)/2, width, height,
82 boxlistWidth, height-infolabelHeight,
83 boxlistWidth, 0, messagelistWidth, height-infolabelHeight,
84 0, height-infolabelHeight, width, infolabelHeight, scaleV(20,18)
87 def __init__(self, session, account):
89 This is the main screen for interacting with the user.
90 It contains the list of mailboxes (boxlist) on the left and
91 the list of messages (messagelist) on the right.
92 At the bottom we have a line for info messages.
93 It is specific for one account.
95 @param session: session in which this screen is running
96 @param account: account for which mailboxes are shown
98 self._session = session
99 self._account = account
100 self.skin = EmailScreen.skin
101 Screen.__init__(self, session)
103 self["actions"] = ActionMap(["InfobarChannelSelection", "WizardActions", "DirectionActions", "MenuActions", "ShortcutActions", "GlobalActions", "HelpActions", "NumberActions", "ChannelSelectBaseActions"],
107 "historyNext": self._selectMessagelist,
108 "historyBack": self._selectBoxlist,
109 "nextBouquet": self._selectMessagelist,
110 "prevBouquet": self._selectBoxlist,
114 "right": self._right,
116 self["messagelist"] = MenuList([], content=eListboxPythonMultiContent)
117 self["messagelist"].l.setItemHeight(scaleV(70, 60))
118 self["messagelist"].l.setFont(0, gFont("Regular", scaleV(20, 18))) # new
119 self["messagelist"].l.setFont(1, gFont("Regular", scaleV(18, 16))) # deleted
120 self["messagelist"].l.setFont(2, gFont("Regular", scaleV(18, 16))) # seen
122 if self._account.isConnected():
123 self["infolabel"] = Label("")
124 self["boxlist"] = MenuList(self._account.mailboxList)
125 self.onLayoutFinish.append(self._finishBoxlist)
127 self["infolabel"] = Label(_("account not connected"))
128 self["boxlist"] = MenuList([])
129 self.currList = "boxlist"
134 def _finishBoxlist(self):
135 # pylint: disable-msg=W0212
136 self.setTitle(_("%(name)s (%(user)s@%(server)s)")
138 'name':self._account._name,
139 'user':self._account._user,
140 'server':self._account._server
142 self["boxlist"].moveToIndex(self._account.inboxPos-1)
143 self._selectBoxlist()
144 self._onBoxSelected()
146 def _selectBoxlist(self):
147 self.currList = "boxlist"
148 self["messagelist"].selectionEnabled(0)
149 self["boxlist"].selectionEnabled(1)
151 def _selectMessagelist(self):
152 self.currList = "messagelist"
153 self["boxlist"].selectionEnabled(0)
154 self["messagelist"].selectionEnabled(1)
157 self[self.currList].up()
160 self[self.currList].down()
163 self[self.currList].pageUp()
166 self[self.currList].pageDown()
169 if self.currList == "boxlist":
170 self._onBoxSelected()
172 self._onMessageSelected()
174 def _ebNotify(self, where, what):
176 Error notification via calling back
177 @param where: location, where the error occurred
178 @param what: message, what happened
180 # pylint: disable-msg=W0212
181 debug("[EmailScreen] _ebNotify error in %s: %s" %(where, what))
182 self.session.open(MessageBox, _("EmailClient for %(account)s:\n\n%(error)s") %{'account': self._account._name, 'error':what}, type=MessageBox.TYPE_ERROR, timeout=config.plugins.emailimap.timeout.value)
184 def _onBoxSelected(self):
185 self["messagelist"].l.setList([])
186 self._onBoxSelectedNoClear()
188 def _onBoxSelectedNoClear(self):
189 self["infolabel"].setText(_("loading headers ..."))
190 if self["boxlist"].getCurrent():
191 if self._account.getMessageList(self._onHeaderList, self["boxlist"].getCurrent()):
192 self._selectMessagelist()
194 self["infolabel"].setText(_("account not connected"))
196 self["infolabel"].setText(_("no mailbox?!?!"))
199 def _onHeaderList(self, result, flagsList):
202 @param result: list of message
203 @param flagsList: list of corresponding flags
205 debug("[EmailScreen] onHeaderList: %s" %len(result))
206 self["infolabel"].setText(_("headers loaded, now parsing ..."))
207 self._flagsList = flagsList
211 # debug("onHeaderList :" + repr(flagsList[m]['FLAGS']))
212 if '\\Seen' in flagsList[m]['FLAGS']:
214 if '\\Deleted' in flagsList[m]['FLAGS']:
215 if not config.plugins.emailimap.showDeleted.value:
219 mylist.append(self._buildMessageListItem(MessageHeader(m, result[m]['RFC822.HEADER']), state))
221 mylist.sort(key=lambda x: x[0].getTimestampUTC(), reverse=True)
222 self["messagelist"].l.setList(mylist)
224 self["infolabel"].setText(_("have %d messages") %(len(mylist)))
226 self["infolabel"].setText(_("have no messages"))
227 # self.onBoxSelected() # brings us into endless loop, when still deleted messages are in there...
228 self._selectBoxlist()
230 def _onMessageSelected(self):
231 self["infolabel"].setText(_("getting message ..."))
232 c = self["messagelist"].getCurrent()
234 if not self._account.getMessage(c[0], self._onMessageLoaded, self._ebNotify):
235 self["infolabel"] = Label(_("account not connected"))
237 def _onMessageLoaded(self, result, message):
238 self["infolabel"].setText(_("parsing message ..."))
239 debug("[EmailScreen] onMessageLoaded") #,result,message
241 msgstr = result[message.uid]['RFC822']
243 self._account.getMessage(message, self._onMessageLoaded, self._ebNotify)
244 # self.loadMessage(message)
246 msg = email.Parser.Parser().parsestr(msgstr) #@UndefinedVariable # pylint: disable-msg=E1101
247 msg.messagebodys = []
250 if msg.is_multipart():
251 for part in msg.walk():
252 if part.get_content_maintype()=="multipart":
254 if part.get_content_maintype() == 'text' and part.get_filename() is None:
255 if part.get_content_subtype() == "html":
256 msg.messagebodys.append(EmailBody(part))
257 elif part.get_content_subtype() == "plain":
258 msg.messagebodys.append(EmailBody(part))
260 debug("[EmailScreen] onMessageLoaded: unknown content type=%s/%s" %(str(part.get_content_maintype()), str(part.get_content_subtype())))
262 debug("[EmailScreen] onMessageLoaded: found Attachment with %s and name %s" %(str(part.get_content_type()), str(part.get_filename())))
263 msg.attachments.append(EmailAttachment(part.get_filename(), part.get_content_type(), part.get_payload()))
265 msg.messagebodys.append(EmailBody(msg))
266 debug("[EmailScreen] onMessageLoaded:" + str(message.uid) +';'+ repr(self._flagsList[message.uid]['FLAGS']))
267 self.session.open(ScreenMailView, self._account, msg, message.uid, self._flagsList[message.uid]['FLAGS']).onHide.append(self._onBoxSelectedNoClear)
268 self["infolabel"].setText("")
270 def _buildMessageListItem(self, message, state):
272 Construct a MultiContentEntryText from parameters
273 @param message: message
274 @param state: IS_UNSEEN (grey), IS_DELETED (red) are especially colored
276 if state == IS_UNSEEN:
278 color = 0x00FFFFFF # white
279 elif state == IS_DELETED:
281 color = 0x00FF6666 # redish :)
284 color = 0x00888888 # grey
287 MultiContentEntryText(pos=(5, 0), size=(self.messagelistWidth, scaleV(20,18)+5), font=font, text=message.getSenderString(), color=color, color_sel=color),
288 MultiContentEntryText(pos=(5, scaleV(20,18)+1), size=(self.messagelistWidth, scaleV(20,18)+5), font=font, text=message.getLocalDateTimeString(), color=color, color_sel=color),
289 MultiContentEntryText(pos=(5, 2*(scaleV(20,18)+1)), size=(self.messagelistWidth, scaleV(20,18)+5), font=font, text=message.getSubject(), color=color, color_sel=color)
292 class ScreenMailView(Screen):
294 def __init__(self, session, account, message, uid, flags):
296 Principal screen to show one mail message.
298 @param account: mail acoount, this message is coming from
299 @param message: the message itself
300 @param uid: uid of the message, needed to (un)delete and unmark
301 @param flags: the flags of the message, needed to check, whether IS_DELETED
303 self._session = session
304 self._email = message
305 self._account = account
306 # debug('ScreenMailView ' + repr(email) + ' dir: ' + repr(dir(email)))
307 width = max(4*140, scaleH(-1, 550))
308 height = scaleV(-1, 476)
309 fontSize = scaleV(24, 20)
310 lineHeight = fontSize+5
311 buttonsGap = (width-4*140)/5
313 <screen position="%d,%d" size="%d,%d" title="view Email" >
314 <widget name="from" position="%d,%d" size="%d,%d" font="Regular;%d" />
315 <widget name="date" position="%d,%d" size="%d,%d" font="Regular;%d" />
316 <widget name="subject" position="%d,%d" size="%d,%d" font="Regular;%d" />
317 <eLabel position="%d,%d" size="%d,2" backgroundColor="#aaaaaa" />
318 <widget name="body" position="%d,%d" size="%d,%d" font="Regular;%d" />
319 <ePixmap position="%d,%d" zPosition="4" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
320 <ePixmap position="%d,%d" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
321 <ePixmap position="%d,%d" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
322 <ePixmap position="%d,%d" zPosition="4" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
323 <widget name="buttonred" position="%d,%d" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;%d" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
324 <widget name="buttongreen" position="%d,%d" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;%d" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
325 <widget name="buttonyellow" position="%d,%d" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;%d" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
326 <widget name="buttonblue" position="%d,%d" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;%d" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
328 (DESKTOP_WIDTH-width)/2, (DESKTOP_HEIGHT-height)/2, width, height,
329 0, 0, width, lineHeight, fontSize-1, # from
330 0, lineHeight, width, lineHeight, fontSize-1, # date
331 0, 2*lineHeight, width, lineHeight, fontSize-1, # subject
332 0, 3*lineHeight+1, width, # line
333 0, 3*lineHeight+5, width, height-3*lineHeight-5-5-30-5, fontSize, # body
334 buttonsGap, height-30-5,
335 2*buttonsGap+140, height-30-5,
336 3*buttonsGap+2*140, height-30-5,
337 4*buttonsGap+3*140, height-30-5,
338 buttonsGap, height-30-5, scaleV(18,16),
339 2*buttonsGap+140, height-30-5, scaleV(18,16),
340 3*buttonsGap+2*140, height-30-5, scaleV(18,16),
341 4*buttonsGap+3*140, height-30-5, scaleV(18,16),
343 Screen.__init__(self, session)
344 self["from"] = Label(decodeHeader(_("From") +": %s" %self._email.get('from', _('no from'))))
345 msgdate = email.utils.parsedate_tz(self._email.get("date", ""))
346 self["date"] = Label(_("Date") +": %s" % (time.ctime(email.utils.mktime_tz(msgdate)) if msgdate else _("no date")))
347 self["subject"] = Label(decodeHeader(_("Subject") +": %s" %self._email.get('subject', _('no subject'))))
348 self["body"] = ScrollLabel(_(self._email.messagebodys[0].getData()))
349 self["buttonred"] = Button("")
350 self["buttongreen"] = Button("")
351 self["buttonyellow"] = Button(_("leave unread"))
352 if '\\Deleted' in flags:
353 self["buttonblue"] = Button(_("undelete"))
355 self["buttonblue"] = Button(_("delete"))
356 self["actions"] = ActionMap(["WizardActions", "DirectionActions", "MenuActions", "ShortcutActions"],
359 "up": self["body"].pageUp,
360 "down": self["body"].pageDown,
361 # TODO: perhaps better use left/right for previous/next message
362 "left": self["body"].pageUp,
363 "right": self["body"].pageDown,
364 "red": self._selectBody,
365 "green": self._selectAttachment,
366 "yellow": self._markUnread,
367 "blue": self._delete,
372 self.onLayoutFinish.append(self._updateButtons)
375 if '\\Deleted' in self._flags:
376 self.session.openWithCallback(self._deleteCB, MessageBox, _("really undelete mail?"), type=MessageBox.TYPE_YESNO, timeout=config.plugins.emailimap.timeout.value)
378 self.session.openWithCallback(self._deleteCB, MessageBox, _("really delete mail?"), type=MessageBox.TYPE_YESNO, timeout=config.plugins.emailimap.timeout.value)
380 def _deleteCB(self, returnValue):
382 if '\\Deleted' in self._flags:
383 if not self._account.undelete(self._uid):
384 self.session.open(MessageBox, _("account not connected"), type=MessageBox.TYPE_INFO, timeout=config.plugins.emailimap.timeout.value)
386 if not self._account.delete(self._uid):
387 self.session.open(MessageBox, _("account not connected"), type=MessageBox.TYPE_INFO, timeout=config.plugins.emailimap.timeout.value)
388 debug("[ScreenMailView] deleteCB: %s" %repr(self._email))
391 def _markUnread(self):
392 if not self._account.unread(self._uid):
393 self.session.open(MessageBox, _("account not connected"), type=MessageBox.TYPE_INFO, timeout=config.plugins.emailimap.timeout.value)
396 def _updateButtons(self):
397 if len(self._email.messagebodys):
398 self["buttonred"].setText(_("Bodys"))
400 self["buttonred"].setText("")
401 if len(self._email.attachments):
402 self["buttongreen"].setText(_("Attachments"))
404 self["buttongreen"].setText("")
406 def _selectBody(self):
407 if len(self._email.messagebodys):
409 for a in self._email.messagebodys:
410 mylist.append((a.getContenttype(), a))
411 self.session.openWithCallback(self._selectBodyCB, ChoiceBox, _("select Body"), mylist)
413 def _selectBodyCB(self, choice):
414 if choice is not None:
415 self["body"].setText(choice[1].getData())
417 def _selectAttachment(self):
418 if len(self._email.attachments):
420 for a in self._email.attachments:
421 name = a.getFilename()
423 mylist.append((a.getFilename(), a))
425 mylist.append((_("no filename"), a))
426 debug("[ScreenMailView] selectAttachment : " + repr(mylist))
427 self.session.openWithCallback(self._selectAttachmentCB, ChoiceBox, _("select Attachment"), mylist)
429 def _selectAttachmentCB(self, choice):
431 if choice[1].getFilename():
432 debug("[ScreenMailView] Attachment selected: " + choice[1].getFilename())
434 debug("[ScreenMailView] Attachment with no filename selected")
435 # nothing happens here. What shall we do now with the attachment?
439 def __init__(self, data):
442 def getEncoding(self):
443 return self.data.get_content_charset()
446 text = self.data.get_payload(decode=True)
447 if self.getEncoding():
449 text = text.decode(self.getEncoding())
450 except UnicodeDecodeError:
452 # debug('EmailBody/getData text: ' + text)
453 #=======================================================================
454 # if self.getEncoding():
455 # text = text.decode(self.getEncoding())
456 #=======================================================================
457 if self.getContenttype() == "text/html":
458 debug("[EmailBody] stripping html")
459 text = strip_readable(text)
460 # debug('EmailBody/getData text: ' + text)
463 return text.encode('utf-8')
464 except UnicodeDecodeError:
468 def getContenttype(self):
469 return self.data.get_content_type()
472 class EmailAttachment:
473 def __init__(self, filename, contenttype, data):
474 self.filename = filename
475 self.contenttype = contenttype
478 def save(self, folder):
480 fp = open(folder+"/"+self.getFilename(),"wb")
484 debug("[EmailAttachment] save %s" %str(e))
488 def getFilename(self):
491 def getContenttype(self):
492 return self.contenttype
497 def UTF7toUTF8(string): # pylint: disable-msg=C0103
498 return imap4.decoder(string)[0]
500 def UTF8toUTF7(string): # pylint: disable-msg=C0103
501 return imap4.encoder(string.decode('utf-8'))[0]
505 def __init__(self, acc):
507 Mail checker object for one account
508 @param acc: the account to be checked periodically, each account has
510 @type acc: EmailAccount
512 # pylint: disable-msg=W0212
514 self._name = acc._name
516 self._timer = eTimer()
517 self._timer.callback.append(self._checkMail)
518 # I guess, users tend to use identical intervals, so make them a bit different :-)
519 # constant stolen from ReconnectingFactory
520 self._interval = int(self._account._interval)*60*1000
521 self._interval = int(random.normalvariate(self._interval, self._interval * 0.11962656472))
522 debug("[CheckMail] %(name)s: __init__: checking all %(interval)s seconds"
523 %{'name':self._name, 'interval':self._interval/1000})
524 self._timer.start(self._interval) # it is minutes
525 self._unseenList = None
529 debug("[CheckMail] %s: exit" %(self._name))
532 def stopChecking(self):
534 Just stop the timer, don't empty the unseenList.
536 debug("[CheckMail] %s: stopChecking" %(self._name))
539 def reStartChecking(self):
541 Start the timer again and immediately do a check.
543 debug("[CheckMail] %s: reStartChecking" %(self._name))
544 self._timer.start(self._interval)
547 def _checkMail(self):
548 # debug("[CheckMail] _checkMail for %s" %self._name)
549 self._account.getUnseenHeaders(self._filterNewUnseen)
551 def _filterNewUnseen(self, newUnseenList):
553 Main method in this class: get the list of unseen messages
554 and check them against the last list. New unseen messages
555 are then displayed via _onHeaderList
556 @param newUnseenList: new list of unseen messages
558 debug('[CheckMail] %s: _filterNewUnseen: %s' %(self._name, repr(newUnseenList)))
559 if self._unseenList is None:
560 debug('[CheckMail] %s: _filterNewUnseen: init' %(self._name))
561 # Notifications.AddNotification(MessageBox, str(len(newUnseenList)) + ' ' + _("unread messages in mailbox %s") %self._name, type=MessageBox.TYPE_INFO, timeout=config.plugins.emailimap.timeout.value)
563 newMessages = filter(lambda x: x not in self._unseenList, newUnseenList)
565 debug("[CheckMail] %s: _filterNewUnseen: new message(s): %s" %(self._name, repr(newMessages)))
566 # construct MessageSet from list of message numbers
567 # newMessageSet = reduce(lambda x,y: y.add(x), newMessages, imap4.MessageSet())
568 newMessageSet = imap4.MessageSet()
569 for i in newMessages:
571 if not self._account.getHeaders(self._onHeaderList, newMessageSet):
572 debug("[CheckMail] %s: _filterNewUnseen: could not get Headers" %(self._name))
574 self._unseenList = newUnseenList
576 def _onHeaderList(self, headers):
578 Notify about the list of headers.
579 @param headers: list of headers
581 # debug("[CheckMail] _onHeaderList headers: %s" %repr(headers))
582 message = _("New mail arrived for account %s:\n\n") %self._name
584 m = MessageHeader(h, headers[h]['RFC822.HEADER'])
585 message += m.getSenderString() + '\n' + m.getSubject() + '\n\n'
586 Notifications.AddNotification(MessageBox, message, type=MessageBox.TYPE_INFO, timeout=config.plugins.emailimap.timeout.value)
588 class MessageHeader(object):
589 def __init__(self, uid, message):
590 self.uid = uid #must be int
591 self.message = email.Parser.HeaderParser().parsestr(message) #@UndefinedVariable # pylint: disable-msg=E1101
593 def getSenderString(self):
594 return decodeHeader(self.get("from"), _("no sender"))
596 def getSubject(self):
597 return decodeHeader(self.get("subject"), _("no subject"))
599 def getLocalDateTimeString(self):
600 msgdate = email.utils.parsedate_tz(self.get("date", ""))
602 return time.ctime(email.utils.mktime_tz(msgdate))
604 return self.get("date", _("no date"))
606 def getTimestampUTC(self):
608 msgdate = email.utils.parsedate_tz(self.get("date", ''))
610 ts = email.utils.mktime_tz(msgdate)
613 def get(self, key, default=None):
614 return self.message.get(key, failobj=default)
617 return "<MessageHeader uid="+str(self.uid)+", subject="+self.getSubject()+">"
619 class EmailAccount():
621 Principal class to hold an account.
623 implements(imap4.IMailboxListener)
625 def __init__(self, params, afterInit=None):
627 Principal class to hold an account.
628 @param params: (name, server, port, user, password, interval, maxmail)
629 @param afterInit: to be called, when init is done. Needed to writeAccounts AFTER this one is added
631 # TODO: decrypt password
632 (self._name, self._server, self._port, self._user, self._password, self._interval, self._maxmail, listall) = params
633 # debug("[EmailAccount] %s: __init__: %s" %(self._name, repr(params)))
634 self._listall = (listall==1)
635 self._factory = createFactory(self, self._user, self._server, int(self._port))
637 self._mailChecker = None
639 self.mailboxList = None
640 self._failureReason = ""
641 self._connectCallback = None
642 mailAccounts.append(self)
647 mailAccounts.remove(self)
648 # stop checker and get rid of it
650 self._mailChecker = None
651 # stop factory and get rid of it
653 self._factory.stopTrying()
654 self._factory = None # I am not sure to stop the factory, though...
655 # if we still have a proto, logout and dump it
660 def isConnected(self):
661 return self._proto is not None and self.mailboxList is not None
663 def forceRetry(self, connectCallback):
665 reset delays and retry
666 @param connectCallback: call this function on successful connect, used by EmailAccountList
668 self._connectCallback = connectCallback
669 if self._factory and self._factory.connector:
670 self._factory.resetDelay()
671 self._factory.retry()
673 self._factory = createFactory(self, self._user, self._server, int(self._port))
675 def removeCallback(self):
676 self._connectCallback = None
679 # TODO: encrypt passwd
680 return (self._name, self._server, self._port, self._user, self._password, self._interval, self._maxmail, (1 if self._listall else 0))
682 def _ebNotify(self, result, where, what):
683 debug("[EmailAccount] %s: _ebNotify error in %s: %s: %s" %(self._name, where, what, result.getErrorMessage()))
684 if config.plugins.emailimap.verbose.value:
685 Notifications.AddNotification(MessageBox, "EmailClient for %(account)s:\n\n%(error)s" %{'account': self._name, 'error':what}, type=MessageBox.TYPE_ERROR, timeout=config.plugins.emailimap.timeout.value)
687 def startChecker(self):
688 # debug("[EmailAccount] %s: startChecker?" %self._name)
689 if int(self._interval) != 0:
690 if self._mailChecker:
691 # so, we already have seen an unseenList
692 # debug("[EmailAccount] %s: startChecker again" %self._name)
693 self._mailChecker.reStartChecking()
695 # debug("[EmailAccount] %s: startChecker new" %self._name)
696 self._mailChecker = CheckMail(self)
698 def stopChecker(self):
699 if self._mailChecker:
700 self._mailChecker.stopChecking()
702 def undelete(self, uid):
705 @param uid: uid of message
708 self._proto.removeFlags(uid, ["\\Deleted"])
713 def delete(self, uid):
715 mark message as deleted
716 @param uid: uid of message
719 self._proto.addFlags(uid, ["\\Deleted"])
724 def unread(self, uid):
726 mark message as unread, remove \\Seen
727 @param uid: uis of message
730 self._proto.removeFlags(uid, ["\\Seen"])
735 def getUnseenHeaders(self, callback):
736 # debug('[EmailAccount] %s: getUnseenHeaders' %self._name)
738 self._proto.examine('inbox').addCallback(self._doSearchUnseen, callback).addErrback(self._ebNotify, "getUnseenHeaders", _("cannot access inbox"))
743 def _doSearchUnseen(self, result, callback): #@UnusedVariable # pylint: disable-msg=W0613
744 # debug('[EmailAccount] %s: _doSearchUnseen' %(self._name))
745 self._proto.search(imap4.Query(unseen=1)).addCallback(callback).addErrback(self._ebNotify, '_doSearchUnseen', _("cannot get list of new messages"))
747 def getMessageList(self, callback, mbox):
749 self._proto.select(mbox.decode('utf-8')).addCallback(self._onSelect, callback).addErrback(self._onSelectFailed, callback, mbox)
754 def _onSelect(self, result, callback):
755 # debug("[EmailAccount] _onExamine: " + str(result))
756 numMessagesinFolder = int(result['EXISTS'])
757 if numMessagesinFolder <= 0:
760 if int(self._maxmail) > 0:
761 maxMessagesToFetch = int(self._maxmail)
762 startmsg = numMessagesinFolder-maxMessagesToFetch+1
765 rangeToFetch = [startmsg, numMessagesinFolder]
767 rangeToFetch = [1, numMessagesinFolder]
769 self._proto.fetchFlags('%i:%i'%(rangeToFetch[0], rangeToFetch[1]) #'1:*'
770 ).addCallback(self._onFlagsList, callback, rangeToFetch)
772 except imap4.IllegalServerResponse, e:
773 debug("[EmailAccount] _onExamine exception: " + str(e))
776 def _onSelectFailed(self, failure, callback, mboxname):
777 debug("[EmailAccount] %s: _onSelectFailed: %s %s" %(self._name, mboxname, str(failure)))
780 def _onFlagsList(self, flagsList, callback, rangeToFetch):
781 self._proto.fetchHeaders('%i:%i'%(rangeToFetch[0], rangeToFetch[1]) #'1:*'
782 ).addCallback(callback, flagsList)
784 def getMessage(self, message, callback, errCallback):
785 debug("[EmailAccount] %s: getMessage: %s" %(self._name, str(message)))
787 self._proto.fetchSize(message.uid
788 ).addCallback(self._onMessageSizeLoaded, message, callback, errCallback
789 ).addErrback(self._onMessageLoadFailed, message, errCallback
795 def _onMessageSizeLoaded(self, result, message, callback, errCallback):
796 debug("[EmailAccount] %s: _onMessageSizeLoaded: %s %s" %(self._name, str(result), str(message)))
797 size = int(result[message.uid]['RFC822.SIZE'])
799 #ask here to open message
800 debug("[EmailAccount] _onMessageSizeLoaded: message to large to open (size=%d)" %size)
801 errCallback('', _("message too large"))
803 self._proto.fetchMessage(message.uid
804 ).addCallback(callback, message,
805 ).addErrback(self._onMessageLoadFailed, message, errCallback
808 def _onMessageLoadFailed(self, failure, message, errCallback):
809 debug("[EmailAccount] %s: onMessageLoadFailed: %s %s" %(self._name, str(failure), str(message)))
810 errCallback('', _("failed to load message") + ': ' + failure.getErrorMessage())
812 def getHeaders(self, callback, messageSet):
813 debug('[EmailAccount] %s: getHeaders' %self._name)
815 self._proto.fetchHeaders(messageSet).addCallback(callback).addErrback(self._ebNotify, 'getHeaders', _("cannot get headers of new messages"))
820 def onConnect(self, proto):
821 debug("[EmailAccount] %s: %s@%s:%s: onConnect" %(self._name, self._user, self._server, self._port))
822 self._factory.resetDelay()
824 self._failureReason = ""
825 if self._connectCallback:
826 self._connectCallback()
827 self._connectCallback = None
828 proto.getCapabilities().addCallback(self._cbCapabilities).addErrback(self._ebCapabilities)
830 def onConnectionFailed(self, reason):
831 debug("[EmailAccount] %s@%s:%s: onConnectFailed: %s" %(self._user, self._server, self._port, reason.getErrorMessage()))
832 reasonString = reason.getErrorMessage()
833 if reasonString != self._failureReason:
834 self._ebNotify(reason, 'onConnectionFailed', _("connection failed - retrying")+'\n'+reason.getErrorMessage())
835 self._failureReason = reasonString
837 # don't retry, if we do not check this account
838 if int(self._interval) == 0 and self._factory:
839 self._factory.stopTrying()
840 # self.stopChecker() not necessary, because we don't have an active connection...
842 def onConnectionLost(self, reason):
843 debug("[EmailAccount] %s@%s:%s: onConnectFailed: %s" %(self._user, self._server, self._port, reason.getErrorMessage()))
844 # too noisy... self._ebNotify(reason, 'onConnectionLost', _("connection lost - retrying"))
847 # don't retry, if we do not check this account
848 if int(self._interval) == 0 and self._factory:
849 self._factory.stopTrying()
851 def _cbCapabilities(self, reason):
852 debug(_("[EmailAccount] %(name)s: _cbCapabilities:\n\
853 ####################################################################################################\n\
854 # If you have problems to log into your imap-server, please send me the output of the following line\n\
855 # cbCapabilities: %(capa)s\n\
856 ####################################################################################################\n")
857 %{'name':self._name, 'capa':str(reason)})
860 def _ebCapabilities(self, reason):
861 debug("[EmailAccount] %s: _ebCapabilities: %s" %(self._name, str(reason)))
864 debug("[EmailAccount] %s: _doLogin secure" %(self._name))
865 d = self._proto.authenticate(self._password)
866 d.addCallback(self._onAuthentication)
867 d.addErrback(self._onAuthenticationFailed)
870 def _onAuthentication(self, result):
871 # better use LSUB here to get only the subscribed to mailboxes
872 debug("[EmailAccount] %s: _onAuthentication: %s" %(self._name, str(result)))
874 self.getMailboxList()
876 def getMailboxList(self):
878 debug("[EmailAccount] %s: getMailboxList list" %(self._name))
879 self._proto.list("", "*").addCallback(self._onMailboxList)
881 debug("[EmailAccount] %s: getMailboxList lsub" %(self._name))
882 self._proto.lsub("", "*").addCallback(self._onMailboxList)
884 def _onAuthenticationFailed(self, failure):
885 # If it failed because no SASL mechanisms match
886 debug("[EmailAccount] %s: onAuthenticationFailed: %s" %(self._name, failure.getErrorMessage()))
888 failure.trap(imap4.NoSupportedAuthentication)
889 self._doLoginInsecure()
891 debug("[EmailAccount] %s: _onAuthenticationFailed: %s" %(self._name, e.message))
894 def _doLoginInsecure(self):
895 debug("[EmailAccount] %s: _doLoginInsecure" %(self._name))
896 self._proto.login(self._user, self._password).addCallback(self._onAuthentication).addErrback(self._onInsecureAuthenticationFailed)
898 def _onInsecureAuthenticationFailed(self, failure):
899 debug("[EmailAccount] %s: _onInsecureAuthenticationFailed: %s" %(self._name, failure.getErrorMessage()))
901 #=======================================================================
902 # Notifications.AddNotification(
904 # _("error logging %(who)s in:\n%(failure)s")
906 # 'who':"%s@%s" %(self._user, self._server),
907 # 'failure':failure.getErrorMessage()
908 # }, type=MessageBox.TYPE_ERROR, timeout=config.plugins.emailimap.timeout.value)
909 #=======================================================================
910 self._ebNotify(failure, "_onInsecureAuthenticationFailed",
911 _("error logging %(who)s in:\n%(failure)s")
913 'who':"%s@%s" %(self._user, self._server),
914 'failure':failure.getErrorMessage()
917 def _onMailboxList(self, result):
918 mylist = [UTF7toUTF8(mb[2]).encode('utf-8') for mb in result if '\\Noselect' not in mb[0]]
919 debug("[EmailAccount] %s: onMailboxList: %s selectable mailboxes" %(self._name, len(mylist)))
920 # debug("[EmailAccount] %s: onMailboxList:\n%s" %(self._name, str(mylist)))
923 self.inboxPos = map(lambda x: x.lower(), mylist).index('inbox')+1
925 debug("[EmailAccount] onMailboxList: no inbox?!?!")
928 self.mailboxList = mylist
930 class EmailAccountList(Screen):
931 # pylint: disable-msg=W0212
932 def __init__(self, session):
934 Entry screen holding the list of accounts.
935 Offering to add, edit or remove one. Also configuration through <menu>
937 debug("[EmailAccountList] __init__")
939 width = max(noButtons*140+35+100, DESKTOP_WIDTH/3)
941 height = max(5*30+50, DESKTOP_HEIGHT/3)
942 buttonsGap = (width-(noButtons)*140-35)/(noButtons+2)
944 <screen position="%d,%d" size="%d,%d" title="Accounts list" >
945 <widget name="accounts" position="0,0" size="%d,%d" scrollbarMode="showOnDemand" />
946 <ePixmap position="%d,%d" zPosition="4" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
947 <ePixmap position="%d,%d" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
948 <ePixmap position="%d,%d" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
949 <ePixmap position="%d,%d" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on" />
950 <widget name="buttonred" position="%d,%d" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;%d" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
951 <widget name="buttongreen" position="%d,%d" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;%d" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
952 <widget name="buttonyellow" position="%d,%d" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;%d" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
954 (DESKTOP_WIDTH-width)/2, (DESKTOP_HEIGHT-height)/2, width, height,
955 width, height, # config
956 buttonsGap, height-45,
957 2*buttonsGap+140, height-45,
958 3*buttonsGap+2*140, height-45,
959 4*buttonsGap+3*140, height-38,
960 buttonsGap, height-45, scaleV(22,18),
961 2*buttonsGap+140, height-45, scaleV(22,18),
962 3*buttonsGap+2*140, height-45, scaleV(22,18)
964 Screen.__init__(self, session)
965 self["buttonred"] = Label(_("remove"))
966 self["buttongreen"] = Label(_("add"))
967 self["buttonyellow"] = Label(_("edit"))
968 self["setupActions"] = ActionMap(["ColorActions", "OkCancelActions", "MenuActions"],
970 "menu": self._config,
973 "yellow": self._edit,
974 "cancel": self._exit,
977 for acc in mailAccounts:
978 if not acc.isConnected():
979 acc.forceRetry(self._layoutFinish)
980 self["accounts"] = MenuList([], content=eListboxPythonMultiContent)
981 self["accounts"].l.setItemHeight(scaleV(20, 18)+5)
982 self["accounts"].l.setFont(0, gFont("Regular", scaleV(20, 18)))
983 self.onLayoutFinish.append(self._layoutFinish)
985 def _layoutFinish(self):
986 self.setTitle(_("Accounts list"))
988 for acc in mailAccounts:
989 if acc.isConnected():
993 accList.append([acc, MultiContentEntryText(pos=(0, 0), size=(self.width, scaleV(20, 18)+5), text=acc._name, color=color, color_sel=color)])
994 self["accounts"].l.setList(accList)
997 debug("[EmailAccountList] _config")
998 self.session.open(EmailConfigOptions, "Rev " + "$Revision$"[11: - 1] + "$Date$"[7:23])
1001 if self["accounts"].getCurrent():
1002 debug("[EmailAccountList] _action: %s" %self["accounts"].getCurrent()[0]._name)
1003 account = self["accounts"].getCurrent()[0]
1004 if account and account.isConnected():
1005 self.session.open(EmailScreen, account)
1006 self._layoutFinish()
1008 self.session.open(MessageBox,
1009 _("account %s is not connected") %self["accounts"].getCurrent()[0]._name,
1010 type=MessageBox.TYPE_INFO,
1011 timeout=config.plugins.emailimap.timeout.value)
1013 debug("[EmailAccountList] _action: no account selected")
1014 self.session.open(MessageBox,
1015 _("no account selected"),
1016 type=MessageBox.TYPE_ERROR,
1017 timeout=config.plugins.emailimap.timeout.value)
1020 debug("[EmailAccountList] _add")
1021 self.session.openWithCallback(self._cbAdd, EmailConfigAccount)
1023 def _cbAdd(self, params):
1025 # TODO: encrypt passwd
1026 EmailAccount(params, writeAccounts)
1030 debug("[EmailAccountList] _edit")
1031 if self["accounts"].getCurrent():
1032 self.session.openWithCallback(self._cbEdit, EmailConfigAccount, self["accounts"].getCurrent()[0].getConfig())
1034 self.session.openWithCallback(self._cbAdd, EmailConfigAccount)
1036 def _cbEdit(self, params):
1038 self["accounts"].getCurrent()[0].exit()
1039 # TODO: encrypt passwd
1040 EmailAccount(params, writeAccounts)
1044 debug("[EmailAccountList] _remove")
1045 if self["accounts"].getCurrent():
1046 self.session.openWithCallback(
1049 _("Really delete account %s?") % self["accounts"].getCurrent()[0]._name)
1051 self.session.open(MessageBox,
1052 _("no account selected"),
1053 type=MessageBox.TYPE_ERROR,
1054 timeout=config.plugins.emailimap.timeout.value)
1056 def _cbRemove(self, ret):
1058 self["accounts"].getCurrent()[0].exit()
1060 self._layoutFinish()
1063 for acc in mailAccounts:
1064 acc.removeCallback()
1067 from Tools.Directories import resolveFilename, SCOPE_SYSETC, SCOPE_CONFIG, SCOPE_PLUGINS
1069 MAILCONF = resolveFilename(SCOPE_CONFIG, "EmailClient.csv")
1072 # we need versioning on the config data
1075 def writeAccounts():
1076 fd = open(MAILCONF, 'w')
1077 fd.write(str(CONFIG_VERSION)+'\n')
1078 out = csv.writer(fd, quotechar='"', lineterminator='\n')
1079 for acc in mailAccounts:
1080 out.writerow(acc.getConfig())
1084 debug("[] getAccounts")
1086 if not os.path.exists(MAILCONF):
1087 fMAILCONF_XML = resolveFilename(SCOPE_SYSETC, "mailconf.xml")
1088 debug("[] getAccounts: check for %s" %fMAILCONF_XML)
1089 if os.path.exists(fMAILCONF_XML):
1090 from xml.dom.minidom import parse
1091 Notifications.AddNotification(MessageBox, _("importing configurations from %s") %fMAILCONF_XML, type=MessageBox.TYPE_INFO, timeout=config.plugins.emailimap.timeout.value)
1092 maildom = parse(fMAILCONF_XML)
1093 for top in maildom.getElementsByTagName("list"):
1094 for acc in top.getElementsByTagName("account"):
1095 name = str(acc.getElementsByTagName("name")[0].childNodes[0].data)
1096 server = str(acc.getElementsByTagName("server")[0].childNodes[0].data)
1097 port = str(acc.getElementsByTagName("port")[0].childNodes[0].data)
1098 user = str(acc.getElementsByTagName("user")[0].childNodes[0].data)
1099 password = str(acc.getElementsByTagName("pass")[0].childNodes[0].data)
1100 interval = str(acc.getElementsByTagName("interval")[0].childNodes[0].data)
1101 maxmail = str(acc.getElementsByTagName("MaxMail")[0].childNodes[0].data)
1102 debug("[EmailClient] - Autostart: import account %s" %acc(name, server, port, user, password, interval, maxmail))
1103 EmailAccount((name, server, port, user, password, interval, maxmail, 0))
1105 debug("[] getAccounts: no file found, exiting")
1107 debug("[] getAccounts: reading %s" %MAILCONF)
1109 accounts = csv.reader(fd, quotechar='"')
1111 for acc in accounts:
1113 version = int(acc[0])
1115 debug("[EmailClient] - Autostart: add account %s" %acc[0])
1117 # add listall param at the end to get version 1
1118 (name, server, port, user, password, interval, maxmail) = acc
1119 acc = (name, server, port, user, password, interval, maxmail, 0)
1122 if version != CONFIG_VERSION:
1125 def main(session, **kwargs): #@UnusedVariable kwargs # pylint: disable-msg=W0613
1126 session.open(EmailAccountList)
1128 def autostart(reason, **kwargs): #@UnusedVariable reason
1129 debug("[EmailClient] - Autostart reason: %d kwargs: %s" %(reason, repr(kwargs)))
1130 debug("[EmailClient] " + "$Revision$"[1:-1] + "$Date$"[7:23] + " starting")
1132 if os.path.isdir('/usr/lib/python2.6') and not os.path.isfile('/usr/lib/python2.6/uu.py'):
1133 shutil.copy(resolveFilename(SCOPE_PLUGINS, "Extensions/EmailClient/uu.py"), '/usr/lib/python2.6/uu.py')
1134 elif os.path.isdir('/usr/lib/python2.5') and not os.path.isfile('/usr/lib/python2.5/uu.py'):
1135 shutil.copy(resolveFilename(SCOPE_PLUGINS, "Extensions/EmailClient/uu.py"), '/usr/lib/python2.5/uu.py')
1140 for acc in mailAccounts:
1145 def Plugins(path, **kwargs): #@UnusedVariable kwargs # pylint: disable-msg=W0613,C0103
1147 PluginDescriptor(name=_("Email Client"), description=_("view Emails via IMAP4"),
1148 where = PluginDescriptor.WHERE_PLUGINMENU,
1152 PluginDescriptor(where=PluginDescriptor.WHERE_SESSIONSTART, fnc=autostart)