1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
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.
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/>.
20 ##############################################################################
24 from imaplib import IMAP4
25 from imaplib import IMAP4_SSL
26 from poplib import POP3
27 from poplib import POP3_SSL
29 import cStringIO as StringIO
35 from openerp import addons
37 from openerp import netsvc
38 from openerp.osv import fields, osv
39 from openerp import tools
40 from openerp.tools.translate import _
42 _logger = logging.getLogger(__name__)
44 class fetchmail_server(osv.osv):
45 """Incoming POP/IMAP mail server account"""
46 _name = 'fetchmail.server'
47 _description = "POP/IMAP Server"
51 'name':fields.char('Name', size=256, required=True, readonly=False),
52 'active':fields.boolean('Active', required=False),
53 'state':fields.selection([
54 ('draft', 'Not Confirmed'),
55 ('done', 'Confirmed'),
56 ], 'Status', select=True, readonly=True),
57 'server' : fields.char('Server Name', size=256, readonly=True, help="Hostname or IP of the mail server", states={'draft':[('readonly', False)]}),
58 'port' : fields.integer('Port', readonly=True, states={'draft':[('readonly', False)]}),
59 'type':fields.selection([
60 ('pop', 'POP Server'),
61 ('imap', 'IMAP Server'),
62 ('local', 'Local Server'),
63 ], 'Server Type', select=True, required=True, readonly=False),
64 'is_ssl':fields.boolean('SSL/TLS', help="Connections are encrypted with SSL/TLS through a dedicated port (default: IMAPS=993, POP3S=995)"),
65 'attach':fields.boolean('Keep Attachments', help="Whether attachments should be downloaded. "
66 "If not enabled, incoming emails will be stripped of any attachments before being processed"),
67 'original':fields.boolean('Keep Original', help="Whether a full original copy of each email should be kept for reference"
68 "and attached to each processed message. This will usually double the size of your message database."),
69 'date': fields.datetime('Last Fetch Date', readonly=True),
70 'user' : fields.char('Username', size=256, readonly=True, states={'draft':[('readonly', False)]}),
71 'password' : fields.char('Password', size=1024, readonly=True, states={'draft':[('readonly', False)]}),
72 'action_id':fields.many2one('ir.actions.server', 'Server Action', help="Optional custom server action to trigger for each incoming mail, "
73 "on the record that was created or updated by this mail"),
74 'object_id': fields.many2one('ir.model', "Create a New Record", help="Process each incoming mail as part of a conversation "
75 "corresponding to this document type. This will create "
76 "new documents for new conversations, or attach follow-up "
77 "emails to the existing conversations (documents)."),
78 'priority': fields.integer('Server Priority', readonly=True, states={'draft':[('readonly', False)]}, help="Defines the order of processing, "
79 "lower values mean higher priority"),
80 'message_ids': fields.one2many('mail.mail', 'fetchmail_server_id', 'Messages', readonly=True),
81 'configuration' : fields.text('Configuration', readonly=True),
82 'script' : fields.char('Script', readonly=True, size=64),
90 'script': '/mail/static/scripts/openerp_mailgate.py',
93 def onchange_server_type(self, cr, uid, ids, server_type=False, ssl=False, object_id=False):
96 if server_type == 'pop':
97 port = ssl and 995 or 110
98 elif server_type == 'imap':
99 port = ssl and 993 or 143
101 values['server'] = ''
102 values['port'] = port
105 'dbname' : cr.dbname,
107 'model' : 'MODELNAME',
110 m = self.pool.get('ir.model')
111 r = m.read(cr,uid,[object_id],['model'])
112 conf['model']=r[0]['model']
113 values['configuration'] = """Use the below script with the following command line options with your Mail Transport Agent (MTA)
115 openerp_mailgate.py --host=HOSTNAME --port=PORT -u %(uid)d -p PASSWORD -d %(dbname)s
117 Example configuration for the postfix mta running locally:
119 /etc/postfix/virtual_aliases:
120 @youdomain openerp_mailgate@localhost
123 openerp_mailgate: "|/path/to/openerp-mailgate.py --host=localhost -u %(uid)d -p PASSWORD -d %(dbname)s"
127 return {'value':values}
129 def set_draft(self, cr, uid, ids, context=None):
130 self.write(cr, uid, ids , {'state':'draft'})
133 def connect(self, cr, uid, server_id, context=None):
134 if isinstance(server_id, (list,tuple)):
135 server_id = server_id[0]
136 server = self.browse(cr, uid, server_id, context)
137 if server.type == 'imap':
139 connection = IMAP4_SSL(server.server, int(server.port))
141 connection = IMAP4(server.server, int(server.port))
142 connection.login(server.user, server.password)
143 elif server.type == 'pop':
145 connection = POP3_SSL(server.server, int(server.port))
147 connection = POP3(server.server, int(server.port))
148 #TODO: use this to remove only unread messages
149 #connection.user("recent:"+server.user)
150 connection.user(server.user)
151 connection.pass_(server.password)
154 def button_confirm_login(self, cr, uid, ids, context=None):
157 for server in self.browse(cr, uid, ids, context=context):
159 connection = server.connect()
160 server.write({'state':'done'})
162 _logger.exception("Failed to connect to %s server %s.", server.type, server.name)
163 raise osv.except_osv(_("Connection test failed!"), _("Here is what we got instead:\n %s.") % tools.ustr(e))
167 if server.type == 'imap':
169 elif server.type == 'pop':
172 # ignored, just a consequence of the previous exception
176 def _fetch_mails(self, cr, uid, ids=False, context=None):
178 ids = self.search(cr, uid, [('state','=','done'),('type','in',['pop','imap'])])
179 return self.fetch_mail(cr, uid, ids, context=context)
181 def fetch_mail(self, cr, uid, ids, context=None):
182 """WARNING: meant for cron usage only - will commit() after each email!"""
185 context['fetchmail_cron_running'] = True
186 mail_thread = self.pool.get('mail.thread')
187 action_pool = self.pool.get('ir.actions.server')
188 for server in self.browse(cr, uid, ids, context=context):
189 _logger.info('start checking for new emails on %s server %s', server.type, server.name)
190 context.update({'fetchmail_server_id': server.id, 'server_type': server.type})
194 if server.type == 'imap':
196 imap_server = server.connect()
198 result, data = imap_server.search(None, '(UNSEEN)')
199 for num in data[0].split():
200 result, data = imap_server.fetch(num, '(RFC822)')
201 res_id = mail_thread.message_process(cr, uid, server.object_id.model,
203 save_original=server.original,
204 strip_attachments=(not server.attach),
206 if res_id and server.action_id:
207 action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id], 'active_model': context.get("thread_model", server.object_id.model)})
208 imap_server.store(num, '+FLAGS', '\\Seen')
211 _logger.info("fetched/processed %s email(s) on %s server %s", count, server.type, server.name)
213 _logger.exception("Failed to fetch mail from %s server %s.", server.type, server.name)
218 elif server.type == 'pop':
220 pop_server = server.connect()
221 (numMsgs, totalSize) = pop_server.stat()
223 for num in range(1, numMsgs + 1):
224 (header, msges, octets) = pop_server.retr(num)
225 msg = '\n'.join(msges)
226 res_id = mail_thread.message_process(cr, uid, server.object_id.model,
228 save_original=server.original,
229 strip_attachments=(not server.attach),
231 if res_id and server.action_id:
232 action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id], 'active_model': context.get("thread_model", server.object_id.model)})
235 _logger.info("fetched/processed %s email(s) on %s server %s", numMsgs, server.type, server.name)
237 _logger.exception("Failed to fetch mail from %s server %s.", server.type, server.name)
241 server.write({'date': time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)})
244 def cron_update(self, cr, uid, context=None):
247 if not context.get('fetchmail_cron_running'):
248 # Enabled/Disable cron based on the number of 'done' server of type pop or imap
249 ids = self.search(cr, uid, [('state','=','done'),('type','in',['pop','imap'])])
251 cron_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fetchmail', 'ir_cron_mail_gateway_action')[1]
252 self.pool.get('ir.cron').write(cr, 1, [cron_id], {'active': bool(ids)})
254 # Nevermind if default cron cannot be found
257 def create(self, cr, uid, values, context=None):
258 res = super(fetchmail_server, self).create(cr, uid, values, context=context)
259 self.cron_update(cr, uid, context=context)
262 def write(self, cr, uid, ids, values, context=None):
263 res = super(fetchmail_server, self).write(cr, uid, ids, values, context=context)
264 self.cron_update(cr, uid, context=context)
267 class mail_mail(osv.osv):
268 _inherit = "mail.mail"
270 'fetchmail_server_id': fields.many2one('fetchmail.server', "Inbound Mail Server",
273 oldname='server_id'),
276 def create(self, cr, uid, values, context=None):
279 fetchmail_server_id = context.get('fetchmail_server_id')
280 if fetchmail_server_id:
281 values['fetchmail_server_id'] = fetchmail_server_id
282 res = super(mail_mail, self).create(cr, uid, values, context=context)
285 def write(self, cr, uid, ids, values, context=None):
288 fetchmail_server_id = context.get('fetchmail_server_id')
289 if fetchmail_server_id:
290 values['fetchmail_server_id'] = fetchmail_server_id
291 res = super(mail_mail, self).write(cr, uid, ids, values, context=context)
295 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: