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 from twisted.internet import protocol |
|
25 from twisted.words.protocols.jabber import error as jab_error |
|
26 from twisted.cred import portal,checkers |
|
27 from twisted.mail import imap4 |
|
28 from email.parser import Parser |
|
29 import email.message |
|
30 import os,os.path |
|
31 from cStringIO import StringIO |
|
32 from twisted.internet import reactor |
|
33 import pdb |
|
34 |
|
35 |
|
36 from zope.interface import implements |
|
37 |
|
38 |
|
39 PLUGIN_INFO = { |
|
40 "name": "IMAP server Plugin", |
|
41 "import_name": "IMAP", |
|
42 "type": "Misc", |
|
43 "protocols": [], |
|
44 "dependencies": ["Maildir"], |
|
45 "main": "IMAP_server", |
|
46 "handler": "no", |
|
47 "description": _("""Create an Imap server that you can use to read your "normal" type messages""") |
|
48 } |
|
49 |
|
50 class IMAP_server(): |
|
51 |
|
52 params = """ |
|
53 <params> |
|
54 <general> |
|
55 <category name="IMAP Server"> |
|
56 <param name="Port" value="10143" type="string" /> |
|
57 </category> |
|
58 </general> |
|
59 </params> |
|
60 """ |
|
61 |
|
62 def __init__(self, host): |
|
63 info(_("Plugin Imap Server initialization")) |
|
64 self.host = host |
|
65 |
|
66 #parameters |
|
67 host.memory.importParams(self.params) |
|
68 |
|
69 port = int(self.host.memory.getParamA("Port", "IMAP Server")) |
|
70 info(_("Launching IMAP server on port %d"), port) |
|
71 |
|
72 self.server_factory = ImapServerFactory(self.host) |
|
73 reactor.listenTCP(port, self.server_factory) |
|
74 |
|
75 class Message(): |
|
76 implements(imap4.IMessage) |
|
77 |
|
78 def __init__(self, uid, mess_fp): |
|
79 debug('Message Init') |
|
80 self.uid=uid |
|
81 self.mess_fp=mess_fp |
|
82 self.message=Parser().parse(mess_fp) |
|
83 |
|
84 def getUID(self): |
|
85 """Retrieve the unique identifier associated with this message. |
|
86 """ |
|
87 debug('getUID (message)') |
|
88 return self.uid |
|
89 |
|
90 def getFlags(self): |
|
91 """Retrieve the flags associated with this message. |
|
92 @return: The flags, represented as strings. |
|
93 """ |
|
94 debug('getFlags') |
|
95 return [] |
|
96 |
|
97 def getInternalDate(self): |
|
98 """Retrieve the date internally associated with this message. |
|
99 @return: An RFC822-formatted date string. |
|
100 """ |
|
101 debug('getInternalDate') |
|
102 return self.message['Date'] |
|
103 |
|
104 |
|
105 def getHeaders(self, negate, *names): |
|
106 """Retrieve a group of message headers. |
|
107 @param names: The names of the headers to retrieve or omit. |
|
108 @param negate: If True, indicates that the headers listed in names |
|
109 should be omitted from the return value, rather than included. |
|
110 @return: A mapping of header field names to header field values |
|
111 """ |
|
112 debug('getHeaders %s - %s' % (negate, names)) |
|
113 final_dict={} |
|
114 to_check=[name.lower() for name in names] |
|
115 for header in self.message.keys(): |
|
116 if (negate and not header.lower() in to_check) or \ |
|
117 (not negate and header.lower() in to_check): |
|
118 final_dict[header]=self.message[header] |
|
119 return final_dict |
|
120 |
|
121 def getBodyFile(self): |
|
122 """Retrieve a file object containing only the body of this message. |
|
123 """ |
|
124 debug('getBodyFile') |
|
125 return StringIO(self.message.get_payload()) |
|
126 |
|
127 def getSize(self): |
|
128 """Retrieve the total size, in octets, of this message. |
|
129 """ |
|
130 debug('getSize') |
|
131 self.mess_fp.seek(0,os.SEEK_END) |
|
132 return self.mess_fp.tell() |
|
133 |
|
134 |
|
135 def isMultipart(self): |
|
136 """Indicate whether this message has subparts. |
|
137 """ |
|
138 debug('isMultipart') |
|
139 return False |
|
140 |
|
141 def getSubPart(self,part): |
|
142 """Retrieve a MIME sub-message |
|
143 @param part: The number of the part to retrieve, indexed from 0. |
|
144 @return: The specified sub-part. |
|
145 """ |
|
146 debug('getSubPart') |
|
147 return TypeError |
|
148 |
|
149 |
|
150 class SatMailbox: |
|
151 implements(imap4.IMailbox) |
|
152 |
|
153 def __init__(self,host,name): |
|
154 self.host = host |
|
155 self.listeners=set() |
|
156 debug ('Mailbox init (%s)', name) |
|
157 if name!="INBOX": |
|
158 raise imap4.MailboxException("Only INBOX is managed for the moment") |
|
159 self.name=name |
|
160 self.mailbox=self.host.plugins["Maildir"].accessMessageBox("INBOX",self.newMessage) |
|
161 |
|
162 def newMessage(self): |
|
163 """Called when a new message is in the mailbox""" |
|
164 debug ("newMessage signal received") |
|
165 nb_messages=self.getMessageCount() |
|
166 for listener in self.listeners: |
|
167 listener.newMessages(nb_messages,None) |
|
168 |
|
169 def getUIDValidity(self): |
|
170 """Return the unique validity identifier for this mailbox. |
|
171 """ |
|
172 debug ('getUIDValidity') |
|
173 return 0 |
|
174 |
|
175 def getUIDNext(self): |
|
176 """Return the likely UID for the next message added to this mailbox. |
|
177 """ |
|
178 debug ('getUIDNext') |
|
179 return self.getMessageCount()+1 |
|
180 |
|
181 def getUID(self,message): |
|
182 """Return the UID of a message in the mailbox |
|
183 @param message: The message sequence number |
|
184 @return: The UID of the message. |
|
185 """ |
|
186 debug ('getUID') |
|
187 return self.mailbox.getId(message) |
|
188 |
|
189 def getMessageCount(self): |
|
190 """Return the number of messages in this mailbox. |
|
191 """ |
|
192 debug('getMessageCount') |
|
193 ret = self.mailbox.getMessageCount() |
|
194 debug("count = %i" % ret) |
|
195 return ret |
|
196 |
|
197 def getRecentCount(self): |
|
198 """Return the number of messages with the 'Recent' flag. |
|
199 """ |
|
200 debug('getRecentCount') |
|
201 return 0 |
|
202 |
|
203 def getUnseenCount(self): |
|
204 """Return the number of messages with the 'Unseen' flag. |
|
205 """ |
|
206 debug('getUnseenCount') |
|
207 return 1 |
|
208 |
|
209 def isWriteable(self): |
|
210 """Get the read/write status of the mailbox. |
|
211 @return: A true value if write permission is allowed, a false value otherwise. |
|
212 """ |
|
213 debug('isWriteable') |
|
214 return True |
|
215 |
|
216 def destroy(self): |
|
217 """Called before this mailbox is deleted, permanently. |
|
218 """ |
|
219 debug('destroy') |
|
220 |
|
221 |
|
222 def requestStatus(self, names): |
|
223 """Return status information about this mailbox. |
|
224 @param names: The status names to return information regarding. |
|
225 The possible values for each name are: MESSAGES, RECENT, UIDNEXT, |
|
226 UIDVALIDITY, UNSEEN. |
|
227 @return: A dictionary containing status information about the |
|
228 requested names is returned. If the process of looking this |
|
229 information up would be costly, a deferred whose callback will |
|
230 eventually be passed this dictionary is returned instead. |
|
231 """ |
|
232 debug('requestStatus') |
|
233 return imap4.statusRequestHelper(self, names) |
|
234 |
|
235 def addListener(self, listener): |
|
236 """Add a mailbox change listener |
|
237 |
|
238 @type listener: Any object which implements C{IMailboxListener} |
|
239 @param listener: An object to add to the set of those which will |
|
240 be notified when the contents of this mailbox change. |
|
241 """ |
|
242 debug('addListener %s' % listener) |
|
243 self.listeners.add(listener) |
|
244 |
|
245 def removeListener(self, listener): |
|
246 """Remove a mailbox change listener |
|
247 |
|
248 @type listener: Any object previously added to and not removed from |
|
249 this mailbox as a listener. |
|
250 @param listener: The object to remove from the set of listeners. |
|
251 |
|
252 @raise ValueError: Raised when the given object is not a listener for |
|
253 this mailbox. |
|
254 """ |
|
255 debug('removeListener') |
|
256 if listener in self.listeners: |
|
257 self.listeners.remove(listener) |
|
258 else: |
|
259 raise imap4.MailboxException('Trying to remove an unknown listener') |
|
260 |
|
261 def addMessage(self, message, flags = (), date = None): |
|
262 """Add the given message to this mailbox. |
|
263 @param message: The RFC822 formatted message |
|
264 @param flags: The flags to associate with this message |
|
265 @param date: If specified, the date to associate with this |
|
266 @return: A deferred whose callback is invoked with the message |
|
267 id if the message is added successfully and whose errback is |
|
268 invoked otherwise. |
|
269 """ |
|
270 debug('addMessage') |
|
271 raise NotImplementedError |
|
272 |
|
273 def expunge(self): |
|
274 """Remove all messages flagged \\Deleted. |
|
275 @return: The list of message sequence numbers which were deleted, |
|
276 or a Deferred whose callback will be invoked with such a list. |
|
277 """ |
|
278 debug('expunge') |
|
279 raise NotImplementedError |
|
280 |
|
281 def fetch(self, messages, uid): |
|
282 """Retrieve one or more messages. |
|
283 @param messages: The identifiers of messages to retrieve information |
|
284 about |
|
285 @param uid: If true, the IDs specified in the query are UIDs; |
|
286 """ |
|
287 debug('fetch (%s, %s)'%(messages,uid)) |
|
288 messages.last = self.getMessageCount() |
|
289 for mes_idx in messages: |
|
290 if mes_idx>self.getMessageCount(): |
|
291 continue |
|
292 yield (mes_idx,Message(mes_idx,self.mailbox.getMessage(mes_idx-1))) |
|
293 |
|
294 def store(self, messages, flags, mode, uid): |
|
295 """Set the flags of one or more messages. |
|
296 @param messages: The identifiers of the messages to set the flags of. |
|
297 @param flags: The flags to set, unset, or add. |
|
298 @param mode: If mode is -1, these flags should be removed from the |
|
299 specified messages. If mode is 1, these flags should be added to |
|
300 the specified messages. If mode is 0, all existing flags should be |
|
301 cleared and these flags should be added. |
|
302 @param uid: If true, the IDs specified in the query are UIDs; |
|
303 otherwise they are message sequence IDs. |
|
304 @return: A dict mapping message sequence numbers to sequences of str |
|
305 representing the flags set on the message after this operation has |
|
306 been performed, or a Deferred whose callback will be invoked with |
|
307 such a dict. |
|
308 """ |
|
309 debug('store') |
|
310 raise NotImplementedError |
|
311 |
|
312 def getFlags(self): |
|
313 """Return the flags defined in this mailbox |
|
314 Flags with the \\ prefix are reserved for use as system flags. |
|
315 @return: A list of the flags that can be set on messages in this mailbox. |
|
316 """ |
|
317 debug('getFlags') |
|
318 #return ['\Seen','\Answered','\Flagged','\Deleted','\Draft', '\Recent'] |
|
319 return [] |
|
320 |
|
321 def getHierarchicalDelimiter(self): |
|
322 """Get the character which delimits namespaces for in this mailbox. |
|
323 """ |
|
324 debug('getHierarchicalDelimiter') |
|
325 return '.' |
|
326 |
|
327 |
|
328 |
|
329 class ImapAccount(imap4.MemoryAccount): |
|
330 #implements(imap4.IAccount) |
|
331 # Actually implement the interface here |
|
332 |
|
333 def __init__(self, host, name): |
|
334 debug("ImapAccount init") |
|
335 self.host=host |
|
336 imap4.MemoryAccount.__init__(self,name) |
|
337 self.addMailbox("Inbox") #We only manage Inbox for the moment |
|
338 debug ('INBOX added') |
|
339 |
|
340 def _emptyMailbox(self, name, id): |
|
341 return SatMailbox(self.host,name) |
|
342 |
|
343 |
|
344 class ImapRealm: |
|
345 implements(portal.IRealm) |
|
346 |
|
347 def __init__(self,host): |
|
348 self.host = host |
|
349 |
|
350 def requestAvatar(self, avatarID, mind, *interfaces): |
|
351 debug('requestAvatar') |
|
352 if imap4.IAccount not in interfaces: |
|
353 raise NotImplementedError |
|
354 return imap4.IAccount, ImapAccount(self.host,avatarID), lambda:None |
|
355 |
|
356 class ImapServerFactory(protocol.ServerFactory): |
|
357 protocol = imap4.IMAP4Server |
|
358 |
|
359 def __init__(self, host): |
|
360 self.host=host |
|
361 |
|
362 def startedConnecting(self, connector): |
|
363 debug (_("IMAP server connection started")) |
|
364 |
|
365 def clientConnectionLost(self, connector, reason): |
|
366 debug (_("IMAP server connection lost (reason: %s)"), reason) |
|
367 |
|
368 def buildProtocol(self, addr): |
|
369 debug ("Building protocole") |
|
370 prot = protocol.ServerFactory.buildProtocol(self, addr) |
|
371 prot.portal = portal.Portal(ImapRealm(self.host)) |
|
372 prot.portal.registerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse(goffi="toto")) |
|
373 return prot |