comparison src/plugins/plugin_misc_maildir.py @ 253:f45ffbf211e9

MAILDIR + IMAP plugins: first draft
author Goffi <goffi@goffi.org>
date Mon, 17 Jan 2011 00:15:50 +0100
parents
children 9fc32d1d9046
comparison
equal deleted inserted replaced
252:c09aa319712e 253:f45ffbf211e9
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)