# -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. import logging import poplib from imaplib import IMAP4, IMAP4_SSL from poplib import POP3, POP3_SSL from flectra import api, fields, models, tools, _ from flectra.exceptions import UserError _logger = logging.getLogger(__name__) MAX_POP_MESSAGES = 50 MAIL_TIMEOUT = 60 # Workaround for Python 2.7.8 bug https://bugs.python.org/issue23906 poplib._MAXLINE = 65536 class FetchmailServer(models.Model): """Incoming POP/IMAP mail server account""" _name = 'fetchmail.server' _description = "POP/IMAP Server" _order = 'priority' name = fields.Char('Name', required=True) active = fields.Boolean('Active', default=True) state = fields.Selection([ ('draft', 'Not Confirmed'), ('done', 'Confirmed'), ], string='Status', index=True, readonly=True, copy=False, default='draft') server = fields.Char(string='Server Name', readonly=True, help="Hostname or IP of the mail server", states={'draft': [('readonly', False)]}) port = fields.Integer(readonly=True, states={'draft': [('readonly', False)]}) type = fields.Selection([ ('pop', 'POP Server'), ('imap', 'IMAP Server'), ('local', 'Local Server'), ], 'Server Type', index=True, required=True, default='pop') is_ssl = fields.Boolean('SSL/TLS', help="Connections are encrypted with SSL/TLS through a dedicated port (default: IMAPS=993, POP3S=995)") attach = fields.Boolean('Keep Attachments', help="Whether attachments should be downloaded. " "If not enabled, incoming emails will be stripped of any attachments before being processed", default=True) original = fields.Boolean('Keep Original', help="Whether a full original copy of each email should be kept for reference " "and attached to each processed message. This will usually double the size of your message database.") date = fields.Datetime(string='Last Fetch Date', readonly=True) user = fields.Char(string='Username', readonly=True, states={'draft': [('readonly', False)]}) password = fields.Char(readonly=True, states={'draft': [('readonly', False)]}) action_id = fields.Many2one('ir.actions.server', string='Server Action', help="Optional custom server action to trigger for each incoming mail, on the record that was created or updated by this mail") object_id = fields.Many2one('ir.model', string="Create a New Record", help="Process each incoming mail as part of a conversation " "corresponding to this document type. This will create " "new documents for new conversations, or attach follow-up " "emails to the existing conversations (documents).") priority = fields.Integer(string='Server Priority', readonly=True, states={'draft': [('readonly', False)]}, help="Defines the order of processing, lower values mean higher priority", default=5) message_ids = fields.One2many('mail.mail', 'fetchmail_server_id', string='Messages', readonly=True) configuration = fields.Text('Configuration', readonly=True) script = fields.Char(readonly=True, default='/mail/static/scripts/openerp_mailgate.py') @api.onchange('type', 'is_ssl', 'object_id') def onchange_server_type(self): self.port = 0 if self.type == 'pop': self.port = self.is_ssl and 995 or 110 elif self.type == 'imap': self.port = self.is_ssl and 993 or 143 else: self.server = '' conf = { 'dbname': self.env.cr.dbname, 'uid': self.env.uid, 'model': self.object_id.model if self.object_id else 'MODELNAME' } self.configuration = """ Use the below script with the following command line options with your Mail Transport Agent (MTA) openerp_mailgate.py --host=HOSTNAME --port=PORT -u %(uid)d -p PASSWORD -d %(dbname)s Example configuration for the postfix mta running locally: /etc/postfix/virtual_aliases: @youdomain openerp_mailgate@localhost /etc/aliases: openerp_mailgate: "|/path/to/openerp-mailgate.py --host=localhost -u %(uid)d -p PASSWORD -d %(dbname)s" """ % conf @api.model def create(self, values): res = super(FetchmailServer, self).create(values) self._update_cron() return res @api.multi def write(self, values): res = super(FetchmailServer, self).write(values) self._update_cron() return res @api.multi def unlink(self): res = super(FetchmailServer, self).unlink() self._update_cron() return res @api.multi def set_draft(self): self.write({'state': 'draft'}) return True @api.multi def connect(self): self.ensure_one() if self.type == 'imap': if self.is_ssl: connection = IMAP4_SSL(self.server, int(self.port)) else: connection = IMAP4(self.server, int(self.port)) connection.login(self.user, self.password) elif self.type == 'pop': if self.is_ssl: connection = POP3_SSL(self.server, int(self.port)) else: connection = POP3(self.server, int(self.port)) #TODO: use this to remove only unread messages #connection.user("recent:"+server.user) connection.user(self.user) connection.pass_(self.password) # Add timeout on socket connection.sock.settimeout(MAIL_TIMEOUT) return connection @api.multi def button_confirm_login(self): for server in self: try: connection = server.connect() server.write({'state': 'done'}) except Exception as err: _logger.info("Failed to connect to %s server %s.", server.type, server.name, exc_info=True) raise UserError(_("Connection test failed: %s") % tools.ustr(err)) finally: try: if connection: if server.type == 'imap': connection.close() elif server.type == 'pop': connection.quit() except Exception: # ignored, just a consequence of the previous exception pass return True @api.model def _fetch_mails(self): """ Method called by cron to fetch mails from servers """ return self.search([('state', '=', 'done'), ('type', 'in', ['pop', 'imap'])]).fetch_mail() @api.multi def fetch_mail(self): """ WARNING: meant for cron usage only - will commit() after each email! """ additionnal_context = { 'fetchmail_cron_running': True } MailThread = self.env['mail.thread'] for server in self: _logger.info('start checking for new emails on %s server %s', server.type, server.name) additionnal_context['fetchmail_server_id'] = server.id additionnal_context['server_type'] = server.type count, failed = 0, 0 imap_server = None pop_server = None if server.type == 'imap': try: imap_server = server.connect() imap_server.select() result, data = imap_server.search(None, '(UNSEEN)') for num in data[0].split(): res_id = None result, data = imap_server.fetch(num, '(RFC822)') imap_server.store(num, '-FLAGS', '\\Seen') try: res_id = MailThread.with_context(**additionnal_context).message_process(server.object_id.model, data[0][1], save_original=server.original, strip_attachments=(not server.attach)) except Exception: _logger.info('Failed to process mail from %s server %s.', server.type, server.name, exc_info=True) failed += 1 if res_id and server.action_id: server.action_id.with_context({ 'active_id': res_id, 'active_ids': [res_id], 'active_model': self.env.context.get("thread_model", server.object_id.model) }).run() imap_server.store(num, '+FLAGS', '\\Seen') self._cr.commit() count += 1 _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed) except Exception: _logger.info("General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True) finally: if imap_server: imap_server.close() imap_server.logout() elif server.type == 'pop': try: while True: pop_server = server.connect() (num_messages, total_size) = pop_server.stat() pop_server.list() for num in range(1, min(MAX_POP_MESSAGES, num_messages) + 1): (header, messages, octets) = pop_server.retr(num) message = (b'\n').join(messages) res_id = None try: res_id = MailThread.with_context(**additionnal_context).message_process(server.object_id.model, message, save_original=server.original, strip_attachments=(not server.attach)) pop_server.dele(num) except Exception: _logger.info('Failed to process mail from %s server %s.', server.type, server.name, exc_info=True) failed += 1 if res_id and server.action_id: server.action_id.with_context({ 'active_id': res_id, 'active_ids': [res_id], 'active_model': self.env.context.get("thread_model", server.object_id.model) }).run() self.env.cr.commit() if num_messages < MAX_POP_MESSAGES: break pop_server.quit() _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", num_messages, server.type, server.name, (num_messages - failed), failed) except Exception: _logger.info("General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True) finally: if pop_server: pop_server.quit() server.write({'date': fields.Datetime.now()}) return True @api.model def _update_cron(self): if self.env.context.get('fetchmail_cron_running'): return try: # Enabled/Disable cron based on the number of 'done' server of type pop or imap cron = self.env.ref('fetchmail.ir_cron_mail_gateway_action') cron.toggle(model=self._name, domain=[('state', '=', 'done'), ('type', 'in', ['pop', 'imap'])]) except ValueError: pass