def _ir_mail_server_send_email(model, message, *args, **kwargs): if '@' not in message['To']: raise AssertionError(model.NO_VALID_RECIPIENT) if sim_error and sim_error == 'send_assert': raise AssertionError('AssertionError') elif sim_error and sim_error == 'send_disconnect': raise SMTPServerDisconnected('SMTPServerDisconnected') elif sim_error and sim_error == 'send_delivery': raise MailDeliveryException('MailDeliveryException') return message['Message-Id']
def send(self, auto_commit=False, raise_exception=False): """ Sends the selected emails immediately, ignoring their current state (mails that have already been sent should not be passed unless they should actually be re-sent). Emails successfully delivered are marked as 'sent', and those that fail to be deliver are marked as 'exception', and the corresponding error mail is output in the server logs. :param bool auto_commit: whether to force a commit of the mail status after sending each mail (meant only for scheduler processing); should never be True during normal transactions (default: False) :param bool raise_exception: whether to raise an exception if the email sending process has failed :return: True """ for server_id, batch_ids in self._split_by_server(): smtp_session = None try: outgoing_obj = self.env['ir.mail_server'].search( [('user_id', '=', self.create_uid.id)], limit=1) if not outgoing_obj: outgoing_obj = self.env['ir.mail_server'].search( [('is_default_server', '=', True)], limit=1) server_id = outgoing_obj.id smtp_session = self.env['ir.mail_server'].connect( mail_server_id=server_id) _logger.error( 'server_id-------------------------------------------------------' ) _logger.error(server_id) _logger.error( 'create_uid-------------------------------------------------------' ) _logger.error(self.create_uid) except Exception as exc: if raise_exception: # To be consistent and backward compatible with mail_mail.send() raised # exceptions, it is encapsulated into an Odoo MailDeliveryException raise MailDeliveryException( _('Unable to connect to SMTP Server'), exc) else: batch = self.browse(batch_ids) batch.write({'state': 'exception', 'failure_reason': exc}) batch._postprocess_sent_message(success_pids=[], failure_type="SMTP") else: self.browse(batch_ids)._send(auto_commit=auto_commit, raise_exception=raise_exception, smtp_session=smtp_session) _logger.info('Sent batch %s emails via mail server ID #%s', len(batch_ids), server_id) finally: if smtp_session: smtp_session.quit()
def _send(self, auto_commit=False, raise_exception=False, smtp_session=None): IrMailServer = self.env['ir.mail_server'] for mail_id in self.ids: try: mail = self.browse(mail_id) if mail.state != 'outgoing': if mail.state != 'exception' and mail.auto_delete: mail.sudo().unlink() continue # load attachment binary data with a separate read(), as prefetching all # `datas` (binary field) could bloat the browse cache, triggerring # soft/hard mem limits with temporary data. attachments = [(a['datas_fname'], base64.b64decode(a['datas']), a['mimetype']) for a in mail.attachment_ids.sudo().read(['datas_fname', 'datas', 'mimetype'])] # specific behavior to customize the send email for notified partners email_list = [] if mail.email_to: email_list.append(mail._send_prepare_values()) for partner in mail.recipient_ids: email_list.append(mail._send_prepare_values(partner=partner)) # headers headers = {} ICP = self.env['ir.config_parameter'].sudo() bounce_alias = ICP.get_param("mail.bounce.alias") catchall_domain = ICP.get_param("mail.catchall.domain") if bounce_alias and catchall_domain: if mail.model and mail.res_id: headers['Return-Path'] = '%s+%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain) else: headers['Return-Path'] = '%s+%d@%s' % (bounce_alias, mail.id, catchall_domain) if mail.headers: try: headers.update(safe_eval(mail.headers)) except Exception: pass # Writing on the mail object may fail (e.g. lock on user) which # would trigger a rollback *after* actually sending the email. # To avoid sending twice the same email, provoke the failure earlier mail.write({ 'state': 'exception', 'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'), }) mail_sent = False # build an RFC2822 email.message.Message object and send it without queuing res = None for email in email_list: msg = IrMailServer.build_email( email_from=mail.email_from, email_to=email.get('email_to'), subject=mail.subject, body=email.get('body'), body_alternative=email.get('body_alternative'), email_cc=tools.email_split(mail.email_cc), reply_to=mail.reply_to, attachments=attachments, message_id=mail.message_id, references=mail.references, object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), subtype='html', subtype_alternative='plain', headers=headers) try: res = IrMailServer.send_email( msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session) except AssertionError as error: if str(error) == IrMailServer.NO_VALID_RECIPIENT: # No valid recipient found for this particular # mail item -> ignore error to avoid blocking # delivery to next recipients, if any. If this is # the only recipient, the mail will show as failed. _logger.info("Ignoring invalid recipients for mail.mail %s: %s", mail.message_id, email.get('email_to')) else: raise if res: mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False}) mail_sent = True # /!\ can't use mail.state here, as mail.refresh() will cause an error # see revid:[email protected] in 6.1 if mail_sent: _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id) mail._postprocess_sent_message(mail_sent=mail_sent) except MemoryError: # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job # instead of marking the mail as failed _logger.exception( 'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option', mail.id, mail.message_id) raise except psycopg2.Error: # If an error with the database occurs, chances are that the cursor is unusable. # This will lead to an `psycopg2.InternalError` being raised when trying to write # `state`, shadowing the original exception and forbid a retry on concurrent # update. Let's bubble it. raise except Exception as e: failure_reason = tools.ustr(e) _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason) mail.write({'state': 'exception', 'failure_reason': failure_reason}) mail._postprocess_sent_message(mail_sent=False) if raise_exception: if isinstance(e, AssertionError): # get the args of the original error, wrap into a value and throw a MailDeliveryException # that is an except_orm, with name and value as arguments value = '. '.join(e.args) raise MailDeliveryException(_("Mail Delivery Failed"), value) raise if auto_commit is True: self._cr.commit() return True
def _send(self, auto_commit=False, raise_exception=False, smtp_session=None): IrMailServer = self.env['ir.mail_server'] IrAttachment = self.env['ir.attachment'] for mail_id in self.ids: success_pids = [] failure_type = None processing_pid = None mail = None try: mail = self.browse(mail_id) if mail.state != 'outgoing': continue # remove attachments if user send the link with the access_token body = mail.body_html or '' attachments = mail.attachment_ids for link in re.findall(r'/web/(?:content|image)/([0-9]+)', body): attachments = attachments - IrAttachment.browse(int(link)) # load attachment binary data with a separate read(), as prefetching all # `datas` (binary field) could bloat the browse cache, triggerring # soft/hard mem limits with temporary data. attachments = [(a['name'], base64.b64decode(a['datas']), a['mimetype']) for a in attachments.sudo().read(['name', 'datas', 'mimetype']) if a['datas'] is not False] # specific behavior to customize the send email for notified partners email_list = [] if mail.email_to: email_list.append(mail._send_prepare_values()) for partner in mail.recipient_ids: values = mail._send_prepare_values(partner=partner) values['partner_id'] = partner email_list.append(values) # headers headers = {} ICP = self.env['ir.config_parameter'].sudo() bounce_alias = ICP.get_param("mail.bounce.alias") catchall_domain = ICP.get_param("mail.catchall.domain") if bounce_alias and catchall_domain: headers['Return-Path'] = '%s@%s' % (bounce_alias, catchall_domain) if mail.headers: try: headers.update(ast.literal_eval(mail.headers)) except Exception: pass # Writing on the mail object may fail (e.g. lock on user) which # would trigger a rollback *after* actually sending the email. # To avoid sending twice the same email, provoke the failure earlier mail.write({ 'state': 'exception', 'failure_reason': _('Error without exception. Probably due to sending an email without computed recipients.'), }) # Update notification in a transient exception state to avoid concurrent # update in case an email bounces while sending all emails related to current # mail record. notifs = self.env['mail.notification'].search([ ('notification_type', '=', 'email'), ('mail_mail_id', 'in', mail.ids), ('notification_status', 'not in', ('sent', 'canceled')) ]) if notifs: notif_msg = _('Error without exception. Probably due to concurrent access update of notification records. Please see with an administrator.') notifs.sudo().write({ 'notification_status': 'exception', 'failure_type': 'unknown', 'failure_reason': notif_msg, }) # `test_mail_bounce_during_send`, force immediate update to obtain the lock. # see rev. 56596e5240ef920df14d99087451ce6f06ac6d36 notifs.flush_recordset(['notification_status', 'failure_type', 'failure_reason']) # build an RFC2822 email.message.Message object and send it without queuing res = None # TDE note: could be great to pre-detect missing to/cc and skip sending it # to go directly to failed state update for email in email_list: msg = IrMailServer.build_email( email_from=mail.email_from, email_to=email.get('email_to'), subject=mail.subject, body=email.get('body'), body_alternative=email.get('body_alternative'), email_cc=tools.email_split(mail.email_cc), reply_to=mail.reply_to, attachments=attachments, message_id=mail.message_id, references=mail.references, object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), subtype='html', subtype_alternative='plain', headers=headers) processing_pid = email.pop("partner_id", None) try: res = IrMailServer.send_email( msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session) if processing_pid: success_pids.append(processing_pid) processing_pid = None except AssertionError as error: if str(error) == IrMailServer.NO_VALID_RECIPIENT: # if we have a list of void emails for email_list -> email missing, otherwise generic email failure if not email.get('email_to') and failure_type != "mail_email_invalid": failure_type = "mail_email_missing" else: failure_type = "mail_email_invalid" # No valid recipient found for this particular # mail item -> ignore error to avoid blocking # delivery to next recipients, if any. If this is # the only recipient, the mail will show as failed. _logger.info("Ignoring invalid recipients for mail.mail %s: %s", mail.message_id, email.get('email_to')) else: raise if res: # mail has been sent at least once, no major exception occurred mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False}) _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id) # /!\ can't use mail.state here, as mail.refresh() will cause an error # see revid:[email protected] in 6.1 mail._postprocess_sent_message(success_pids=success_pids, failure_type=failure_type) except MemoryError: # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job # instead of marking the mail as failed _logger.exception( 'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option', mail.id, mail.message_id) # mail status will stay on ongoing since transaction will be rollback raise except (psycopg2.Error, smtplib.SMTPServerDisconnected): # If an error with the database or SMTP session occurs, chances are that the cursor # or SMTP session are unusable, causing further errors when trying to save the state. _logger.exception( 'Exception while processing mail with ID %r and Msg-Id %r.', mail.id, mail.message_id) raise except Exception as e: failure_reason = tools.ustr(e) _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason) mail.write({'state': 'exception', 'failure_reason': failure_reason}) mail._postprocess_sent_message(success_pids=success_pids, failure_reason=failure_reason, failure_type='unknown') if raise_exception: if isinstance(e, (AssertionError, UnicodeEncodeError)): if isinstance(e, UnicodeEncodeError): value = "Invalid text: %s" % e.object else: value = '. '.join(e.args) raise MailDeliveryException(value) raise if auto_commit is True: self._cr.commit() return True
def test_mail_mail_send_exceptions_raise_management(self): """ Test various use case with exceptions and errors and see how they are managed and stored at mail and notification level. """ mail, notification = self.test_mail, self.test_notification mail.write({ 'email_from': '*****@*****.**', 'email_to': '*****@*****.**' }) # SMTP connecting issues with self.mock_mail_gateway(): _connect_current = self.connect_mocked.side_effect # classic errors that may be raised during sending, just to test their current support for error, msg in [ (smtplib.SMTPServerDisconnected('SMTPServerDisconnected'), 'SMTPServerDisconnected'), (smtplib.SMTPResponseException('code', 'SMTPResponseException'), 'code\nSMTPResponseException'), (smtplib.SMTPNotSupportedError('SMTPNotSupportedError'), 'SMTPNotSupportedError'), (smtplib.SMTPException('SMTPException'), 'SMTPException'), (SSLError('SSLError'), 'SSLError'), (gaierror('gaierror'), 'gaierror'), (timeout('timeout'), 'timeout') ]: def _connect(*args, **kwargs): raise error self.connect_mocked.side_effect = _connect mail.send(raise_exception=False) self.assertFalse(mail.failure_type) self.assertEqual(mail.failure_reason, msg) self.assertEqual(mail.state, 'exception') self.assertEqual(notification.failure_type, 'mail_smtp') self.assertEqual(notification.notification_status, 'exception') self._reset_data() self.connect_mocked.side_effect = _connect_current # SMTP sending issues with self.mock_mail_gateway(): _send_current = self.send_email_mocked.side_effect self._reset_data() mail.write({'email_to': '*****@*****.**'}) # should always raise for those errors, even with raise_exception=False for error, error_class in [ (smtplib.SMTPServerDisconnected("Some exception"), smtplib.SMTPServerDisconnected), (MemoryError("Some exception"), MemoryError) ]: def _send_email(*args, **kwargs): raise error self.send_email_mocked.side_effect = _send_email with self.assertRaises(error_class): mail.send(raise_exception=False) self.assertFalse( mail.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback' ) self.assertFalse( mail.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback' ) self.assertEqual( mail.state, 'outgoing', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback' ) self.assertFalse( notification.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback' ) self.assertEqual( notification.notification_status, 'ready', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback' ) # MailDeliveryException: should be catched; other issues are sub-catched under # a MailDeliveryException and are catched for error, msg in [ (MailDeliveryException("Some exception"), 'Some exception'), (ValueError("Unexpected issue"), 'Unexpected issue') ]: def _send_email(*args, **kwargs): raise error self.send_email_mocked.side_effect = _send_email self._reset_data() mail.send(raise_exception=False) self.assertEqual(mail.failure_reason, msg) self.assertFalse(mail.failure_type, 'Mail: unlogged failure type to fix') self.assertEqual(mail.state, 'exception') self.assertEqual(notification.failure_type, 'unknown', 'Mail: generic failure type') self.assertEqual(notification.notification_status, 'exception') self.send_email_mocked.side_effect = _send_current
def send_email( self, message, mail_server_id=None, smtp_server=None, smtp_port=None, smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False, smtp_session=None, ): """Override the standard method to fix the issue of using a mail client where relaying is disallowed.""" # Use the default bounce address **only if** no Return-Path was # provided by caller. Caller may be using Variable Envelope Return # Path (VERP) to detect no-longer valid email addresses. smtp_from = (message["Return-Path"] or self._get_default_bounce_address() or message["From"]) assert ( smtp_from ), "The Return-Path or From header is required for any outbound email" # The email's "Envelope From" (Return-Path), and all recipient # addresses must only contain ASCII characters. from_rfc2822 = extract_rfc2822_addresses(smtp_from) assert from_rfc2822, ("Malformed 'Return-Path' or 'From' address: " "%r - " "It should contain one valid plain ASCII " "email") % smtp_from # use last extracted email, to support rarities like 'Support@MyComp # <*****@*****.**>' smtp_from = from_rfc2822[-1] email_to = message["To"] email_cc = message["Cc"] email_bcc = message["Bcc"] del message["Bcc"] smtp_to_list = [ address for base in [email_to, email_cc, email_bcc] for address in extract_rfc2822_addresses(base) if address ] assert smtp_to_list, self.NO_VALID_RECIPIENT x_forge_to = message["X-Forge-To"] if x_forge_to: # `To:` header forged, e.g. for posting on mail.channels, # to avoid confusion del message["X-Forge-To"] del message["To"] # avoid multiple To: headers! message["To"] = x_forge_to # Do not actually send emails in testing mode! if (getattr(threading.currentThread(), "testing", False) or self.env.registry.in_test_mode()): _test_logger.info("skip sending email in test mode") return message["Message-Id"] try: message_id = message["Message-Id"] # START OF CODE ADDED smtp = self.connect( smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption or False, smtp_debug, ) from email.utils import parseaddr, formataddr # exact name and address (oldname, oldemail) = parseaddr(message["From"]) # use original name with new address newfrom = formataddr((oldname, smtp.user)) # need to use replace_header instead '=' to prevent # double field message.replace_header("From", newfrom) smtp.sendmail(smtp.user, smtp_to_list, message.as_string()) # END OF CODE ADDED # do not quit() a pre-established smtp_session if not smtp_session: smtp.quit() except smtplib.SMTPServerDisconnected: raise except Exception as e: params = (ustr(smtp_server), e.__class__.__name__, ustr(e)) msg = _( "Mail delivery failed via SMTP server '%s'.\n%s: %s") % params _logger.info(msg) raise MailDeliveryException(_("Mail Delivery Failed"), msg) return message_id
def send( self, auto_commit=False, raise_exception=False, company=None, ): """ 立即发送选定的电子邮件,而忽略它们的当前状态(除非已被重新发送,否则不应该传递已经发送的电子邮件)。 成功发送的电子邮件被标记为“已发送”,未发送成功的电子邮件被标记为“例外”,并且相应的错误邮件将输出到服务器日志中。 :param bool auto_commit: 在发送每封邮件后是否强制提交邮件状态(仅用于调度程序处理); 在正常传递中,永远不应该为True(默认值:False) :param bool raise_exception: 如果电子邮件发送过程失败,将引发异常 :param bool is_wecom_message: 标识是企业微信消息 :param company: 公司 :return: True """ if not company: company = self.env.company for server_id, batch_ids in self._split_by_server(): smtp_session = None try: if self.is_wecom_message: # 标识为企业微信消息的 mail_mail 对象 pass掉 # print("mail.mail-send()-1") pass else: smtp_session = self.env["ir.mail_server"].connect( mail_server_id=server_id ) except Exception as exc: if self.is_wecom_message: # 标识为企业微信消息的 mail_mail 对象 pass掉 print("mail.mail-send()-2") pass else: if raise_exception: # 为了与 mail_mail.send() 引发的异常保持一致并向后兼容,它被封装到一个Odoo MailDeliveryException中 raise MailDeliveryException( _("Unable to connect to SMTP Server"), exc ) else: batch = self.browse(batch_ids) batch.write({"state": "exception", "failure_reason": exc}) batch._postprocess_sent_message( success_pids=[], failure_type="SMTP" ) else: if self.is_wecom_message: self.browse(batch_ids)._send_wecom_message( auto_commit=auto_commit, raise_exception=raise_exception, company=company, ) _logger.info("Sent batch %s messages via Wecom", len(batch_ids)) else: self.browse(batch_ids)._send( auto_commit=auto_commit, raise_exception=raise_exception, smtp_session=smtp_session, ) _logger.info( "Sent batch %s emails via mail server ID #%s", len(batch_ids), server_id, ) finally: if self.is_wecom_message: # 标识为企业微信消息的 mail_mail 对象 pass掉 pass else: if smtp_session: smtp_session.quit()
def _send(self, auto_commit=False, raise_exception=False, smtp_session=None): IrMailServer = self.env['ir.mail_server'] IrAttachment = self.env['ir.attachment'] for mail_id in self.ids: success_pids = [] failure_type = None processing_pid = None mail = None try: mail = self.browse(mail_id) if mail.state != 'outgoing': if mail.state != 'exception' and mail.auto_delete: mail.sudo().unlink() continue # remove attachments if user send the link with the access_token body = mail.body_html or '' attachments = mail.attachment_ids for link in re.findall(r'/web/(?:content|image)/([0-9]+)', body): attachments = attachments - IrAttachment.browse(int(link)) # load attachment binary data with a separate read(), as prefetching all # `datas` (binary field) could bloat the browse cache, triggerring # soft/hard mem limits with temporary data. attachments = [(a['datas_fname'], base64.b64decode(a['datas']), a['mimetype']) for a in attachments.sudo().read( ['datas_fname', 'datas', 'mimetype'])] # specific behavior to customize the send email for notified partners email_list = [] if mail.email_to: email_list.append(mail._send_prepare_values()) for partner in mail.recipient_ids: values = mail._send_prepare_values(partner=partner) values['partner_id'] = partner email_list.append(values) if mail.cc_visible: email_cc_list = [] for partner_cc in mail.recipient_cc_ids: email_to = formataddr( (partner_cc.name or 'False', partner_cc.email or 'False')) email_cc_list.append(email_to) # Convert Email List To String For BCC & CC email_cc_string = ','.join(email_cc_list) else: email_cc_string = '' if mail.bcc_visible: email_bcc_list = [] for partner_bcc in mail.recipient_bcc_ids: email_to = formataddr( (partner_bcc.name or 'False', partner_bcc.email or 'False')) email_bcc_list.append(email_to) # Convert Email List To String For BCC & CC email_bcc_string = ','.join(email_bcc_list) else: email_bcc_string = '' # headers headers = {} ICP = self.env['ir.config_parameter'].sudo() bounce_alias = ICP.get_param("mail.bounce.alias") catchall_domain = ICP.get_param("mail.catchall.domain") if bounce_alias and catchall_domain: if mail.model and mail.res_id: headers['Return-Path'] = '%s+%d-%s-%d@%s' % ( bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain) else: headers['Return-Path'] = '%s+%d@%s' % ( bounce_alias, mail.id, catchall_domain) if mail.headers: try: headers.update(safe_eval(mail.headers)) except Exception: pass # Writing on the mail object may fail (e.g. lock on user) which # would trigger a rollback *after* actually sending the email. # To avoid sending twice the same email, provoke the failure earlier mail.write({ 'state': 'exception', 'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.' ), }) # Update notification in a transient exception state to avoid concurrent # update in case an email bounces while sending all emails related to current # mail record. notifs = self.env['mail.notification'].search([ ('is_email', '=', True), ('mail_id', 'in', mail.ids), ('email_status', 'not in', ('sent', 'canceled')) ]) if notifs: notif_msg = _( 'Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.' ) notifs.write({ 'email_status': 'exception', 'failure_type': 'UNKNOWN', 'failure_reason': notif_msg, }) # build an RFC2822 email.message.Message object and send it without queuing res = None for email in email_list: msg = IrMailServer.build_email( email_from=mail.email_from, email_to=email.get('email_to'), subject=mail.subject, body=email.get('body'), body_alternative=email.get('body_alternative'), email_cc=tools.email_split_and_format(email_cc_string), email_bcc=tools.email_split_and_format( email_bcc_string), reply_to=mail.reply_to, attachments=attachments, message_id=mail.message_id, references=mail.references, object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), subtype='html', subtype_alternative='plain', headers=headers) processing_pid = email.pop("partner_id", None) try: res = IrMailServer.send_email( msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session) if processing_pid: success_pids.append(processing_pid) processing_pid = None except AssertionError as error: if str(error) == IrMailServer.NO_VALID_RECIPIENT: failure_type = "RECIPIENT" # No valid recipient found for this particular # mail item -> ignore error to avoid blocking # delivery to next recipients, if any. If this is # the only recipient, the mail will show as failed. _logger.info( "Ignoring invalid recipients for mail.mail %s: %s", mail.message_id, email.get('email_to')) else: raise if res: # mail has been sent at least once, no major exception occured mail.write({ 'state': 'sent', 'message_id': res, 'failure_reason': False }) _logger.info( 'Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id) # /!\ can't use mail.state here, as mail.refresh() will cause an error # see revid:[email protected] in 6.1 mail._postprocess_sent_message(success_pids=success_pids, failure_type=failure_type) except MemoryError: # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job # instead of marking the mail as failed _logger.exception( 'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option', mail.id, mail.message_id) # mail status will stay on ongoing since transaction will be rollback raise except psycopg2.Error: # If an error with the database occurs, chances are that the cursor is unusable. # This will lead to an `psycopg2.InternalError` being raised when trying to write # `state`, shadowing the original exception and forbid a retry on concurrent # update. Let's bubble it. raise except Exception as e: failure_reason = tools.ustr(e) _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason) mail.write({ 'state': 'exception', 'failure_reason': failure_reason }) mail._postprocess_sent_message(success_pids=success_pids, failure_reason=failure_reason, failure_type='UNKNOWN') if raise_exception: if isinstance(e, (AssertionError, UnicodeEncodeError)): if isinstance(e, UnicodeEncodeError): value = "Invalid text: %s" % e.object else: # get the args of the original error, wrap into a value and throw a MailDeliveryException # that is an except_orm, with name and value as arguments value = '. '.join(e.args) raise MailDeliveryException(_("Mail Delivery Failed"), value) raise if auto_commit is True: self._cr.commit() return True
def process_email_queue(self, ids=None): """Send immediately queued messages, committing after each message is sent - this is not transactional and should not be called during another transaction! :param list ids: optional list of emails ids to send. If passed no search is performed, and these ids are used instead. :param dict context: if a 'filters' key is present in context, this value will be used as an additional filter to further restrict the outgoing messages to send (by default all 'outgoing' messages are sent). """ res = None for server_id in self.env['ir.mail_server'].search([]): smtp_session = None raise_exception = False if not self.ids: batch_total_max = 0 sys_params = self.env['ir.config_parameter'].sudo() limit = int(sys_params.get_param('mail.session.batch.size', 28)) ids = [] _logger.info("======= sending emails with limit=%s" % limit) filters = [ '&', '&', ('state', '=', 'outgoing'), ('mail_server_id', '=', server_id.id), '|', ('scheduled_date', '<', datetime.datetime.now()), ('scheduled_date', '=', False) ] if 'filters' in self._context: filters.extend(self._context['filters']) ids = self.search(filters, limit=int(limit)).ids ids.sort() _logger.error( '*********** PROCESANDO SERVER MAIL RECORDS ************') _logger.error(ids) try: smtp_session = self.env['ir.mail_server'].connect( mail_server_id=server_id.id) except Exception as exc: if raise_exception: # To be consistent and backward compatible with mail_mail.send() raised # exceptions, it is encapsulated into an Odoo MailDeliveryException raise MailDeliveryException( _('Unable to connect to SMTP Server'), exc) else: batch = self.browse(ids) batch.write({'state': 'exception', 'failure_reason': exc}) batch._postprocess_sent_message(success_pids=[], failure_type="SMTP") else: auto_commit = not getattr(threading.currentThread(), 'testing', False) self.browse(ids)._send(auto_commit=auto_commit, raise_exception=raise_exception, smtp_session=smtp_session) _logger.info('Sent batch %s emails via mail server ID #%s', len(ids), server_id) finally: if smtp_session: smtp_session.quit()
def send_email(self, message, mail_server_id=None, smtp_server=None, smtp_port=None, smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False, smtp_session=None): """Sends an email directly (no queuing). No retries are done, the caller should handle MailDeliveryException in order to ensure that the mail is never lost. If the mail_server_id is provided, sends using this mail server, ignoring other smtp_* arguments. If mail_server_id is None and smtp_server is None, use the default mail server (highest priority). If mail_server_id is None and smtp_server is not None, use the provided smtp_* arguments. If both mail_server_id and smtp_server are None, look for an 'smtp_server' value in server config, and fails if not found. :param message: the email.message.Message to send. The envelope sender will be extracted from the ``Return-Path`` (if present), or will be set to the default bounce address. The envelope recipients will be extracted from the combined list of ``To``, ``CC`` and ``BCC`` headers. :param smtp_session: optional pre-established SMTP session. When provided, overrides `mail_server_id` and all the `smtp_*` parameters. Passing the matching `mail_server_id` may yield better debugging/log messages. The caller is in charge of disconnecting the session. :param mail_server_id: optional id of ir.mail_server to use for sending. overrides other smtp_* arguments. :param smtp_server: optional hostname of SMTP server to use :param smtp_encryption: optional TLS mode, one of 'none', 'starttls' or 'ssl' (see ir.mail_server fields for explanation) :param smtp_port: optional SMTP port, if mail_server_id is not passed :param smtp_user: optional SMTP user, if mail_server_id is not passed :param smtp_password: optional SMTP password to use, if mail_server_id is not passed :param smtp_debug: optional SMTP debug flag, if mail_server_id is not passed :return: the Message-ID of the message that was just sent, if successfully sent, otherwise raises MailDeliveryException and logs root cause. """ # Use the default bounce address **only if** no Return-Path was # provided by caller. Caller may be using Variable Envelope Return # Path (VERP) to detect no-longer valid email addresses. smtp_from = message['Return-Path'] or self._get_default_bounce_address( ) or message['From'] assert smtp_from, "The Return-Path or From header is required for any outbound email" # The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters. from_rfc2822 = extract_rfc2822_addresses(smtp_from) assert from_rfc2822, ( "Malformed 'Return-Path' or 'From' address: %r - " "It should contain one valid plain ASCII email") % smtp_from # use last extracted email, to support rarities like 'Support@MyComp <*****@*****.**>' smtp_from = '*****@*****.**' email_to = message['To'] email_cc = message['Cc'] email_bcc = message['Bcc'] del message['Bcc'] smtp_to_list = [ address for base in [email_to, email_cc, email_bcc] for address in extract_rfc2822_addresses(base) if address ] assert smtp_to_list, self.NO_VALID_RECIPIENT x_forge_to = message['X-Forge-To'] if x_forge_to: # `To:` header forged, e.g. for posting on mail.channels, to avoid confusion del message['X-Forge-To'] del message['To'] # avoid multiple To: headers! message['To'] = x_forge_to # Do not actually send emails in testing mode! if getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode(): _test_logger.info("skip sending email in test mode") return message['Message-Id'] try: message_id = message['Message-Id'] smtp = smtp_session smtp = smtp or self.connect(smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption, smtp_debug, mail_server_id=mail_server_id) if sys.version_info < (3, 7, 4): # header folding code is buggy and adds redundant carriage # returns, it got fixed in 3.7.4 thanks to bpo-34424 message_str = message.as_string() message_str = re.sub('\r+(?!\n)', '', message_str) mail_options = [] if any( (not is_ascii(addr) for addr in smtp_to_list + [smtp_from])): # non ascii email found, require SMTPUTF8 extension, # the relay may reject it mail_options.append("SMTPUTF8") smtp.sendmail(smtp_from, smtp_to_list, message_str, mail_options=mail_options) else: smtp.send_message(message, smtp_from, smtp_to_list) # do not quit() a pre-established smtp_session if not smtp_session: smtp.quit() except smtplib.SMTPServerDisconnected: raise except Exception as e: params = (ustr(smtp_server), e.__class__.__name__, ustr(e)) msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s", *params) _logger.info(msg) raise MailDeliveryException(_("Mail Delivery Failed"), msg) return message_id