253
|
1 #!/usr/bin/python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 """ |
|
5 SAT plugin for managing imap server |
|
6 Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org) |
|
7 |
|
8 This program is free software: you can redistribute it and/or modify |
|
9 it under the terms of the GNU General Public License as published by |
|
10 the Free Software Foundation, either version 3 of the License, or |
|
11 (at your option) any later version. |
|
12 |
|
13 This program is distributed in the hope that it will be useful, |
|
14 but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
16 GNU General Public License for more details. |
|
17 |
|
18 You should have received a copy of the GNU General Public License |
|
19 along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
20 """ |
|
21 |
|
22 from logging import debug, info, error |
|
23 import warnings |
|
24 warnings.filterwarnings('ignore','the MimeWriter',DeprecationWarning,'twisted' ) #FIXME: to be removed, see http://twistedmatrix.com/trac/ticket/4038 |
|
25 from twisted.internet import protocol |
|
26 from twisted.words.protocols.jabber import error as jab_error |
|
27 from twisted.cred import portal,checkers |
|
28 from twisted.mail import imap4,maildir |
|
29 from email.parser import Parser |
|
30 import email.message |
|
31 from email.charset import Charset |
|
32 import os,os.path |
|
33 from cStringIO import StringIO |
|
34 from twisted.internet import reactor |
|
35 import pdb |
|
36 |
|
37 |
|
38 from zope.interface import implements |
|
39 |
|
40 |
|
41 PLUGIN_INFO = { |
|
42 "name": "Maildir Plugin", |
|
43 "import_name": "Maildir", |
|
44 "type": "Misc", |
|
45 "protocols": [], |
|
46 "dependencies": [], |
|
47 "main": "MaildirBox", |
|
48 "handler": "no", |
|
49 "description": _("""Intercept "normal" type messages, and put them in a Maildir type box""") |
|
50 } |
|
51 |
|
52 MAILDIR_PATH = "Maildir" |
|
53 |
|
54 class MaildirError(Exception): |
|
55 pass |
|
56 |
|
57 class MaildirBox(): |
|
58 |
|
59 def __init__(self, host): |
|
60 info(_("Plugin Maildir initialization")) |
|
61 self.host = host |
|
62 |
|
63 self.__observed={} |
|
64 self.__mailboxes={} |
|
65 |
|
66 #the trigger |
|
67 host.trigger.add("MessageReceived", self.MessageReceivedTrigger) |
|
68 |
|
69 def accessMessageBox(self, boxname, observer=None): |
|
70 """Create and return a MailboxUser instance |
|
71 @param boxname: name of the box |
|
72 @param observer: method to call when a NewMessage arrive""" |
|
73 if not self.__mailboxes.has_key(boxname): |
|
74 self.__mailboxes[boxname]=MailboxUser(self, boxname, observer) |
|
75 else: |
|
76 if observer: |
|
77 self.addObserver(observer, boxname) |
|
78 return self.__mailboxes[boxname] |
|
79 |
|
80 def _removeBoxAccess(self, boxname, mailboxUser): |
|
81 """Remove a reference to a box |
|
82 @param name: name of the box |
|
83 @param mailboxUser: MailboxUser instance""" |
|
84 if not self.__mailboxes.has_key(boxname): |
|
85 err_msg=_("Trying to remove an mailboxUser not referenced") |
|
86 error(_("INTERNAL ERROR: ") + err_msg) |
|
87 raise MaildirError(err_msg) |
|
88 assert self.__mailboxes[boxname]==mailboxUser |
|
89 del __mailboxes[boxname] |
|
90 |
|
91 def _checkBoxReference(self, boxname): |
|
92 """Check if there is a reference on a box, and return it |
|
93 @param boxname: name of the box to check |
|
94 @return: MailboxUser instance or None""" |
|
95 if self.__mailboxes.has_key(boxname): |
|
96 return self.__mailboxes[boxname] |
|
97 |
|
98 |
|
99 |
|
100 def MessageReceivedTrigger(self, message): |
|
101 """This trigger catch normal message and put the in the Maildir box. |
|
102 If the message is not of "normal" type, do nothing |
|
103 @param message: message xmlstrem |
|
104 @return: False if it's a normal message, True else""" |
|
105 for e in message.elements(): |
|
106 if e.name == "body": |
|
107 type = message['type'] if message.hasAttribute('type') else 'chat' #FIXME: check specs |
|
108 if message['type'] != 'normal': |
|
109 return True |
|
110 self.accessMessageBox("INBOX").addMessage(message) |
|
111 return False |
|
112 |
|
113 def addObserver(self, callback, boxname, signal="NEW_MESSAGE"): |
|
114 """Add an observer for maildir box changes |
|
115 @param callback: method to call when the the box is updated |
|
116 @param boxname: name of the box to observe |
|
117 @param signal: which signal is observed by the caller""" |
|
118 if not self.__observed.has_key(boxname): |
|
119 self.__observed[boxname]={} |
|
120 if not self.__observed[boxname].has_key(signal): |
|
121 self.__observed[boxname][signal]=set() |
|
122 self.__observed[boxname][signal].add(callback) |
|
123 |
|
124 def removeObserver(self, callback, boxname, signal="NEW_MESSAGE"): |
|
125 """Remove an observer of maildir box changes |
|
126 @param callback: method to remove from obervers |
|
127 @param boxname: name of the box which was observed |
|
128 @param signal: which signal was observed by the caller""" |
|
129 if not self.__observed.has_key(boxname): |
|
130 err_msg=_("Trying to remove an observer for an inexistant mailbox") |
|
131 error(_("INTERNAL ERROR: ") + err_msg) |
|
132 raise MaildirError(err_msg) |
|
133 if not self.__observed[boxname].has_key(signal): |
|
134 err_msg=_("Trying to remove an inexistant observer, no observer for this signal") |
|
135 error(_("INTERNAL ERROR: ") + err_msg) |
|
136 raise MaildirError(err_msg) |
|
137 if not callback in self.__observed[boxname][signal]: |
|
138 err_msg=_("Trying to remove an inexistant observer") |
|
139 error(_("INTERNAL ERROR: ") + err_msg) |
|
140 raise MaildirError(err_msg) |
|
141 self.__observed[boxname][signal].remove(callback) |
|
142 |
|
143 def emitSignal(self, boxname, signal_name): |
|
144 """Emit the signal to observer""" |
|
145 debug('emitSignal %s %s' %(boxname, signal_name)) |
|
146 try: |
|
147 for observer_cb in self.__observed[boxname][signal_name]: |
|
148 observer_cb() |
|
149 except KeyError: |
|
150 pass |
|
151 |
|
152 |
|
153 class MailboxUser: |
|
154 """This class is used to access a mailbox""" |
|
155 |
|
156 def xmppMessage2mail(self, message): |
|
157 """Convert the XMPP's XML message to a basic rfc2822 message |
|
158 @param xml: domish.Element of the message |
|
159 @return: string email""" |
|
160 mail = email.message.Message() |
|
161 mail['MIME-Version'] = "1.0" |
|
162 mail['Content-Type'] = "text/plain; charset=UTF-8; format=flowed" |
|
163 mail['Content-Transfer-Encoding'] = "8bit" |
|
164 mail['From'] = message['from'].encode('utf-8') |
|
165 mail['To'] = message['to'].encode('utf-8') |
|
166 mail['Date'] = email.utils.formatdate().encode('utf-8') |
|
167 #TODO: save thread id |
|
168 for e in message.elements(): |
|
169 if e.name == "body": |
|
170 mail.set_payload(e.children[0].encode('utf-8')) |
|
171 elif e.name == "subject": |
|
172 mail['Subject'] = e.children[0].encode('utf-8') |
|
173 return mail.as_string() |
|
174 |
|
175 def __init__(self, _maildir, name, observer=None): |
|
176 """@param _maildir: the main MaildirBox instance |
|
177 @param name: name of the mailbox |
|
178 THIS OBJECT MUST NOT BE USED DIRECTLY: use MaildirBox.accessMessageBox instead""" |
|
179 if _maildir._checkBoxReference(self): |
|
180 error ("INTERNAL ERROR: MailboxUser MUST NOT be instancied directly") |
|
181 raise MailboxException('double MailboxUser instanciation') |
|
182 if name!="INBOX": |
|
183 raise NotImplementedError |
|
184 self.name=name |
|
185 self.maildir=_maildir |
|
186 mailbox_path = os.path.expanduser(os.path.join(self.maildir.host.get_const('local_dir'), MAILDIR_PATH)) |
|
187 self.mailbox_path=mailbox_path |
|
188 self.mailbox = maildir.MaildirMailbox(mailbox_path) |
|
189 self.observer=observer |
|
190 if observer: |
|
191 debug("adding observer for %s" % name) |
|
192 self.maildir.addObserver(observer, name, "NEW_MESSAGE") |
|
193 |
|
194 def __destroy__(self): |
|
195 if observer: |
|
196 debug("removing observer for %s" % self.name) |
|
197 self._maildir.removeObserver(observer, self.name, "NEW_MESSAGE") |
|
198 self._maildir._removeBoxAccess(self.name, self) |
|
199 |
|
200 def addMessage(self, message): |
|
201 """Add a message to the box |
|
202 @param message: XMPP XML message""" |
|
203 self.mailbox.appendMessage(self.xmppMessage2mail(message)).addCallback(self.emitSignal, "NEW_MESSAGE") |
|
204 |
|
205 def emitSignal(self, ignore, signal): |
|
206 """Emit the signal to the observers""" |
|
207 print ('self: %s, mailbox: %s, count: %i' % (self, self.mailbox, self.getMessageCount())) |
|
208 self.maildir.emitSignal(self.name, signal) |
|
209 |
|
210 def getId(self, mess_idx): |
|
211 """Return the Unique ID of the message |
|
212 @mess_idx: message index""" |
|
213 return self.mailbox.getUidl(mess_idx) |
|
214 |
|
215 def getMessageCount(self): |
|
216 """Return number of mails present in this box""" |
|
217 print "count: %i" % len(self.mailbox.listMessages()) |
|
218 return len(self.mailbox.listMessages()) |
|
219 |
|
220 def getMessage(self, mess_idx): |
|
221 """Return the full message |
|
222 @mess_idx: message index""" |
|
223 return self.mailbox.getMessage(mess_idx) |