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