Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_imap.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_misc_imap.py@33c8c4973743 |
children | 56f94936df1e |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SàT plugin for managing imap server | |
5 # Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.i18n import _ | |
21 from sat.core.constants import Const as C | |
22 from sat.core.log import getLogger | |
23 log = getLogger(__name__) | |
24 from twisted.internet import protocol, defer | |
25 from twisted.cred import portal, checkers, credentials | |
26 from twisted.cred import error as cred_error | |
27 from twisted.mail import imap4 | |
28 from twisted.python import failure | |
29 from email.parser import Parser | |
30 import os | |
31 from cStringIO import StringIO | |
32 from twisted.internet import reactor | |
33 | |
34 from zope.interface import implements | |
35 | |
36 PLUGIN_INFO = { | |
37 C.PI_NAME: "IMAP server Plugin", | |
38 C.PI_IMPORT_NAME: "IMAP", | |
39 C.PI_TYPE: "Misc", | |
40 C.PI_PROTOCOLS: [], | |
41 C.PI_DEPENDENCIES: ["Maildir"], | |
42 C.PI_MAIN: "IMAP_server", | |
43 C.PI_HANDLER: "no", | |
44 C.PI_DESCRIPTION: _("""Create an Imap server that you can use to read your "normal" type messages""") | |
45 } | |
46 | |
47 | |
48 class IMAP_server(object): | |
49 #TODO: connect profile on mailbox request, once password is accepted | |
50 | |
51 params = """ | |
52 <params> | |
53 <general> | |
54 <category name="Mail Server"> | |
55 <param name="IMAP Port" value="10143" type="int" constraint="1;65535" /> | |
56 </category> | |
57 </general> | |
58 </params> | |
59 """ | |
60 | |
61 def __init__(self, host): | |
62 log.info(_("Plugin Imap Server initialization")) | |
63 self.host = host | |
64 | |
65 #parameters | |
66 host.memory.updateParams(self.params) | |
67 | |
68 port = int(self.host.memory.getParamA("IMAP Port", "Mail Server")) | |
69 log.info(_("Launching IMAP server on port %d") % port) | |
70 | |
71 self.server_factory = ImapServerFactory(self.host) | |
72 reactor.listenTCP(port, self.server_factory) | |
73 | |
74 | |
75 class Message(object): | |
76 implements(imap4.IMessage) | |
77 | |
78 def __init__(self, uid, flags, mess_fp): | |
79 log.debug('Message Init') | |
80 self.uid = uid | |
81 self.flags = flags | |
82 self.mess_fp = mess_fp | |
83 self.message = Parser().parse(mess_fp) | |
84 | |
85 def getUID(self): | |
86 """Retrieve the unique identifier associated with this message. | |
87 """ | |
88 log.debug('getUID (message)') | |
89 return self.uid | |
90 | |
91 def getFlags(self): | |
92 """Retrieve the flags associated with this message. | |
93 @return: The flags, represented as strings. | |
94 """ | |
95 log.debug('getFlags') | |
96 return self.flags | |
97 | |
98 def getInternalDate(self): | |
99 """Retrieve the date internally associated with this message. | |
100 @return: An RFC822-formatted date string. | |
101 """ | |
102 log.debug('getInternalDate') | |
103 return self.message['Date'] | |
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 log.debug(u'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 log.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 log.debug('getSize') | |
131 self.mess_fp.seek(0, os.SEEK_END) | |
132 return self.mess_fp.tell() | |
133 | |
134 def isMultipart(self): | |
135 """Indicate whether this message has subparts. | |
136 """ | |
137 log.debug('isMultipart') | |
138 return False | |
139 | |
140 def getSubPart(self, part): | |
141 """Retrieve a MIME sub-message | |
142 @param part: The number of the part to retrieve, indexed from 0. | |
143 @return: The specified sub-part. | |
144 """ | |
145 log.debug('getSubPart') | |
146 return TypeError | |
147 | |
148 | |
149 class SatMailbox(object): | |
150 implements(imap4.IMailbox) | |
151 | |
152 def __init__(self, host, name, profile): | |
153 self.host = host | |
154 self.listeners = set() | |
155 log.debug(u'Mailbox init (%s)' % name) | |
156 if name != "INBOX": | |
157 raise imap4.MailboxException("Only INBOX is managed for the moment") | |
158 self.mailbox = self.host.plugins["Maildir"].accessMessageBox(name, self.messageNew, profile) | |
159 | |
160 def messageNew(self): | |
161 """Called when a new message is in the mailbox""" | |
162 log.debug("messageNew signal received") | |
163 nb_messages = self.getMessageCount() | |
164 for listener in self.listeners: | |
165 listener.newMessages(nb_messages, None) | |
166 | |
167 def getUIDValidity(self): | |
168 """Return the unique validity identifier for this mailbox. | |
169 """ | |
170 log.debug('getUIDValidity') | |
171 return 0 | |
172 | |
173 def getUIDNext(self): | |
174 """Return the likely UID for the next message added to this mailbox. | |
175 """ | |
176 log.debug('getUIDNext') | |
177 return self.mailbox.getNextUid() | |
178 | |
179 def getUID(self, message): | |
180 """Return the UID of a message in the mailbox | |
181 @param message: The message sequence number | |
182 @return: The UID of the message. | |
183 """ | |
184 log.debug(u'getUID (%i)' % message) | |
185 #return self.mailbox.getUid(message-1) #XXX: it seems that this method get uid and not message sequence number | |
186 return message | |
187 | |
188 def getMessageCount(self): | |
189 """Return the number of messages in this mailbox. | |
190 """ | |
191 log.debug('getMessageCount') | |
192 ret = self.mailbox.getMessageCount() | |
193 log.debug("count = %i" % ret) | |
194 return ret | |
195 | |
196 def getRecentCount(self): | |
197 """Return the number of messages with the 'Recent' flag. | |
198 """ | |
199 log.debug('getRecentCount') | |
200 return len(self.mailbox.getMessageIdsWithFlag('\\Recent')) | |
201 | |
202 def getUnseenCount(self): | |
203 """Return the number of messages with the 'Unseen' flag. | |
204 """ | |
205 log.debug('getUnseenCount') | |
206 return self.getMessageCount() - len(self.mailbox.getMessageIdsWithFlag('\\SEEN')) | |
207 | |
208 def isWriteable(self): | |
209 """Get the read/write status of the mailbox. | |
210 @return: A true value if write permission is allowed, a false value otherwise. | |
211 """ | |
212 log.debug('isWriteable') | |
213 return True | |
214 | |
215 def destroy(self): | |
216 """Called before this mailbox is deleted, permanently. | |
217 """ | |
218 log.debug('destroy') | |
219 | |
220 def requestStatus(self, names): | |
221 """Return status information about this mailbox. | |
222 @param names: The status names to return information regarding. | |
223 The possible values for each name are: MESSAGES, RECENT, UIDNEXT, | |
224 UIDVALIDITY, UNSEEN. | |
225 @return: A dictionary containing status information about the | |
226 requested names is returned. If the process of looking this | |
227 information up would be costly, a deferred whose callback will | |
228 eventually be passed this dictionary is returned instead. | |
229 """ | |
230 log.debug('requestStatus') | |
231 return imap4.statusRequestHelper(self, names) | |
232 | |
233 def addListener(self, listener): | |
234 """Add a mailbox change listener | |
235 | |
236 @type listener: Any object which implements C{IMailboxListener} | |
237 @param listener: An object to add to the set of those which will | |
238 be notified when the contents of this mailbox change. | |
239 """ | |
240 log.debug(u'addListener %s' % listener) | |
241 self.listeners.add(listener) | |
242 | |
243 def removeListener(self, listener): | |
244 """Remove a mailbox change listener | |
245 | |
246 @type listener: Any object previously added to and not removed from | |
247 this mailbox as a listener. | |
248 @param listener: The object to remove from the set of listeners. | |
249 | |
250 @raise ValueError: Raised when the given object is not a listener for | |
251 this mailbox. | |
252 """ | |
253 log.debug('removeListener') | |
254 if listener in self.listeners: | |
255 self.listeners.remove(listener) | |
256 else: | |
257 raise imap4.MailboxException('Trying to remove an unknown listener') | |
258 | |
259 def addMessage(self, message, flags=(), date=None): | |
260 """Add the given message to this mailbox. | |
261 @param message: The RFC822 formatted message | |
262 @param flags: The flags to associate with this message | |
263 @param date: If specified, the date to associate with this | |
264 @return: A deferred whose callback is invoked with the message | |
265 id if the message is added successfully and whose errback is | |
266 invoked otherwise. | |
267 """ | |
268 log.debug('addMessage') | |
269 raise imap4.MailboxException("Client message addition not implemented yet") | |
270 | |
271 def expunge(self): | |
272 """Remove all messages flagged \\Deleted. | |
273 @return: The list of message sequence numbers which were deleted, | |
274 or a Deferred whose callback will be invoked with such a list. | |
275 """ | |
276 log.debug('expunge') | |
277 self.mailbox.removeDeleted() | |
278 | |
279 def fetch(self, messages, uid): | |
280 """Retrieve one or more messages. | |
281 @param messages: The identifiers of messages to retrieve information | |
282 about | |
283 @param uid: If true, the IDs specified in the query are UIDs; | |
284 """ | |
285 log.debug(u'fetch (%s, %s)' % (messages, uid)) | |
286 if uid: | |
287 messages.last = self.mailbox.getMaxUid() | |
288 messages.getnext = self.mailbox.getNextExistingUid | |
289 for mess_uid in messages: | |
290 if mess_uid is None: | |
291 log.debug('stopping iteration') | |
292 raise StopIteration | |
293 try: | |
294 yield (mess_uid, Message(mess_uid, self.mailbox.getFlagsUid(mess_uid), self.mailbox.getMessageUid(mess_uid))) | |
295 except IndexError: | |
296 continue | |
297 else: | |
298 messages.last = self.getMessageCount() | |
299 for mess_idx in messages: | |
300 if mess_idx > self.getMessageCount(): | |
301 raise StopIteration | |
302 yield (mess_idx, Message(mess_idx, self.mailbox.getFlags(mess_idx), self.mailbox.getMessage(mess_idx - 1))) | |
303 | |
304 def store(self, messages, flags, mode, uid): | |
305 """Set the flags of one or more messages. | |
306 @param messages: The identifiers of the messages to set the flags of. | |
307 @param flags: The flags to set, unset, or add. | |
308 @param mode: If mode is -1, these flags should be removed from the | |
309 specified messages. If mode is 1, these flags should be added to | |
310 the specified messages. If mode is 0, all existing flags should be | |
311 cleared and these flags should be added. | |
312 @param uid: If true, the IDs specified in the query are UIDs; | |
313 otherwise they are message sequence IDs. | |
314 @return: A dict mapping message sequence numbers to sequences of str | |
315 representing the flags set on the message after this operation has | |
316 been performed, or a Deferred whose callback will be invoked with | |
317 such a dict. | |
318 """ | |
319 log.debug('store') | |
320 | |
321 flags = [flag.upper() for flag in flags] | |
322 | |
323 def updateFlags(getF, setF): | |
324 ret = {} | |
325 for mess_id in messages: | |
326 if (uid and mess_id is None) or (not uid and mess_id > self.getMessageCount()): | |
327 break | |
328 _flags = set(getF(mess_id) if mode else []) | |
329 if mode == -1: | |
330 _flags.difference_update(set(flags)) | |
331 else: | |
332 _flags.update(set(flags)) | |
333 new_flags = list(_flags) | |
334 setF(mess_id, new_flags) | |
335 ret[mess_id] = tuple(new_flags) | |
336 return ret | |
337 | |
338 if uid: | |
339 messages.last = self.mailbox.getMaxUid() | |
340 messages.getnext = self.mailbox.getNextExistingUid | |
341 ret = updateFlags(self.mailbox.getFlagsUid, self.mailbox.setFlagsUid) | |
342 for listener in self.listeners: | |
343 listener.flagsChanged(ret) | |
344 return ret | |
345 | |
346 else: | |
347 messages.last = self.getMessageCount() | |
348 ret = updateFlags(self.mailbox.getFlags, self.mailbox.setFlags) | |
349 newFlags = {} | |
350 for idx in ret: | |
351 #we have to convert idx to uid for the listeners | |
352 newFlags[self.mailbox.getUid(idx)] = ret[idx] | |
353 for listener in self.listeners: | |
354 listener.flagsChanged(newFlags) | |
355 return ret | |
356 | |
357 def getFlags(self): | |
358 """Return the flags defined in this mailbox | |
359 Flags with the \\ prefix are reserved for use as system flags. | |
360 @return: A list of the flags that can be set on messages in this mailbox. | |
361 """ | |
362 log.debug('getFlags') | |
363 return ['\\SEEN', '\\ANSWERED', '\\FLAGGED', '\\DELETED', '\\DRAFT'] # TODO: add '\\RECENT' | |
364 | |
365 def getHierarchicalDelimiter(self): | |
366 """Get the character which delimits namespaces for in this mailbox. | |
367 """ | |
368 log.debug('getHierarchicalDelimiter') | |
369 return '.' | |
370 | |
371 | |
372 class ImapSatAccount(imap4.MemoryAccount): | |
373 #implements(imap4.IAccount) | |
374 | |
375 def __init__(self, host, profile): | |
376 log.debug("ImapAccount init") | |
377 self.host = host | |
378 self.profile = profile | |
379 imap4.MemoryAccount.__init__(self, profile) | |
380 self.addMailbox("Inbox") # We only manage Inbox for the moment | |
381 log.debug('INBOX added') | |
382 | |
383 def _emptyMailbox(self, name, id): | |
384 return SatMailbox(self.host, name, self.profile) | |
385 | |
386 | |
387 class ImapRealm(object): | |
388 implements(portal.IRealm) | |
389 | |
390 def __init__(self, host): | |
391 self.host = host | |
392 | |
393 def requestAvatar(self, avatarID, mind, *interfaces): | |
394 log.debug('requestAvatar') | |
395 profile = avatarID.decode('utf-8') | |
396 if imap4.IAccount not in interfaces: | |
397 raise NotImplementedError | |
398 return imap4.IAccount, ImapSatAccount(self.host, profile), lambda: None | |
399 | |
400 | |
401 class SatProfileCredentialChecker(object): | |
402 """ | |
403 This credential checker check against SàT's profile and associated jabber's password | |
404 Check if the profile exists, and if the password is OK | |
405 Return the profile as avatarId | |
406 """ | |
407 implements(checkers.ICredentialsChecker) | |
408 credentialInterfaces = (credentials.IUsernamePassword, | |
409 credentials.IUsernameHashedPassword) | |
410 | |
411 def __init__(self, host): | |
412 self.host = host | |
413 | |
414 def _cbPasswordMatch(self, matched, profile): | |
415 if matched: | |
416 return profile.encode('utf-8') | |
417 else: | |
418 return failure.Failure(cred_error.UnauthorizedLogin()) | |
419 | |
420 def requestAvatarId(self, credentials): | |
421 profiles = self.host.memory.getProfilesList() | |
422 if not credentials.username in profiles: | |
423 return defer.fail(cred_error.UnauthorizedLogin()) | |
424 d = self.host.memory.asyncGetParamA("Password", "Connection", profile_key=credentials.username) | |
425 d.addCallback(lambda password: credentials.checkPassword(password)) | |
426 d.addCallback(self._cbPasswordMatch, credentials.username) | |
427 return d | |
428 | |
429 | |
430 class ImapServerFactory(protocol.ServerFactory): | |
431 protocol = imap4.IMAP4Server | |
432 | |
433 def __init__(self, host): | |
434 self.host = host | |
435 | |
436 def startedConnecting(self, connector): | |
437 log.debug(_("IMAP server connection started")) | |
438 | |
439 def clientConnectionLost(self, connector, reason): | |
440 log.debug(_(u"IMAP server connection lost (reason: %s)"), reason) | |
441 | |
442 def buildProtocol(self, addr): | |
443 log.debug("Building protocol") | |
444 prot = protocol.ServerFactory.buildProtocol(self, addr) | |
445 prot.portal = portal.Portal(ImapRealm(self.host)) | |
446 prot.portal.registerChecker(SatProfileCredentialChecker(self.host)) | |
447 return prot |