2011-07-22 16:34:57 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# OpenERP, Open Source Management Solution
|
2012-03-13 13:59:49 +00:00
|
|
|
# Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
|
2011-07-22 16:34:57 +00:00
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
2012-08-15 13:36:43 +00:00
|
|
|
# FP Note: can we remove some dependencies ? Use lint
|
|
|
|
|
2011-07-22 16:34:57 +00:00
|
|
|
import base64
|
2011-08-09 23:44:28 +00:00
|
|
|
import dateutil.parser
|
2011-07-22 16:34:57 +00:00
|
|
|
import email
|
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
import time
|
2012-02-06 17:19:11 +00:00
|
|
|
import datetime
|
2011-07-22 16:34:57 +00:00
|
|
|
from email.header import decode_header
|
2011-09-07 15:13:48 +00:00
|
|
|
from email.message import Message
|
2011-07-22 16:34:57 +00:00
|
|
|
|
|
|
|
from osv import osv
|
|
|
|
from osv import fields
|
2012-05-10 13:54:47 +00:00
|
|
|
import pytz
|
|
|
|
from tools import DEFAULT_SERVER_DATETIME_FORMAT
|
|
|
|
import tools
|
2011-07-22 16:34:57 +00:00
|
|
|
|
2012-06-22 06:48:54 +00:00
|
|
|
_logger = logging.getLogger(__name__)
|
2011-07-22 16:34:57 +00:00
|
|
|
|
2012-07-06 09:41:41 +00:00
|
|
|
""" Some tools for parsing / creating email fields """
|
2011-07-22 16:34:57 +00:00
|
|
|
def decode(text):
|
|
|
|
"""Returns unicode() string conversion of the the given encoded smtp header text"""
|
|
|
|
if text:
|
|
|
|
text = decode_header(text.replace('\r', ''))
|
|
|
|
return ''.join([tools.ustr(x[0], x[1]) for x in text])
|
|
|
|
|
2012-07-06 09:41:41 +00:00
|
|
|
def mail_tools_to_email(text):
|
2011-07-22 16:34:57 +00:00
|
|
|
"""Return a list of the email addresses found in ``text``"""
|
|
|
|
if not text: return []
|
|
|
|
return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
|
|
|
|
|
2012-05-08 13:56:00 +00:00
|
|
|
class mail_message(osv.Model):
|
2012-04-20 09:53:01 +00:00
|
|
|
"""Model holding messages: system notification (replacing res.log
|
2012-08-15 17:08:22 +00:00
|
|
|
notifications), comments (for OpenChatter feature). This model also
|
|
|
|
provides facilities to parse new email messages. Type of messages are
|
|
|
|
differentiated using the 'type' column. """
|
2011-07-22 16:34:57 +00:00
|
|
|
|
|
|
|
_name = 'mail.message'
|
2012-08-15 17:08:22 +00:00
|
|
|
_description = 'Message'
|
|
|
|
_order = 'id desc'
|
2011-07-22 16:34:57 +00:00
|
|
|
|
2012-08-15 17:08:22 +00:00
|
|
|
def get_record_name(self, cr, uid, ids, name, arg, context=None):
|
|
|
|
result = dict.fromkeys(ids, '')
|
|
|
|
for message in self.browse(cr, uid, ids, context=context):
|
|
|
|
if not message.model or not message.res_id:
|
|
|
|
continue
|
|
|
|
result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1]
|
|
|
|
return result
|
|
|
|
|
|
|
|
def name_get(self, cr, uid, ids, context=None):
|
2012-08-10 14:43:39 +00:00
|
|
|
# name_get may receive int id instead of an id list
|
|
|
|
if isinstance(ids, (int, long)):
|
|
|
|
ids = [ids]
|
2012-08-15 17:08:22 +00:00
|
|
|
res = []
|
|
|
|
for message in self.browse(cr, uid, ids, context=context):
|
|
|
|
name = ''
|
|
|
|
if message.subject:
|
|
|
|
name = '%s: ' % (message.subject)
|
|
|
|
if message.body_text:
|
|
|
|
name = name + message.body_text[0:20]
|
|
|
|
res.append((message.id, name))
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
2011-07-22 16:34:57 +00:00
|
|
|
_columns = {
|
2012-08-15 17:08:22 +00:00
|
|
|
# should we keep a distinction between email and comment ?
|
2012-04-02 16:24:25 +00:00
|
|
|
'type': fields.selection([
|
2012-06-25 13:42:53 +00:00
|
|
|
('email', 'email'),
|
2012-04-02 16:24:25 +00:00
|
|
|
('comment', 'Comment'),
|
|
|
|
('notification', 'System notification'),
|
2012-07-20 09:25:45 +00:00
|
|
|
], 'Type',
|
|
|
|
help="Message type: email for email message, notification for system "\
|
|
|
|
"message, comment for other messages such as user replies"),
|
2012-08-15 13:36:43 +00:00
|
|
|
|
2012-08-15 17:08:22 +00:00
|
|
|
'author_id': fields.many2one('res.partner', 'Author', required=True),
|
2012-08-15 18:44:03 +00:00
|
|
|
# this is redundant with notifications ? Yes
|
2012-07-12 15:42:59 +00:00
|
|
|
'partner_ids': fields.many2many('res.partner',
|
2012-08-10 14:43:39 +00:00
|
|
|
'mail_message_res_partner_rel',
|
2012-07-12 15:42:59 +00:00
|
|
|
'message_id', 'partner_id', 'Destination partners',
|
|
|
|
help="When sending emails through the social network composition wizard"\
|
|
|
|
"you may choose to send a copy of the mail to partners."),
|
2012-08-15 18:44:03 +00:00
|
|
|
'attachment_ids': fields.one2many('ir.attachment', 'res_id', 'Attachments'
|
|
|
|
domain=[('res_model','=','mail.message')]),
|
2012-08-15 13:36:43 +00:00
|
|
|
|
2012-07-20 09:25:45 +00:00
|
|
|
'parent_id': fields.many2one('mail.message', 'Parent Message',
|
|
|
|
select=True, ondelete='set null',
|
|
|
|
help="Parent message, used for displaying as threads with hierarchy"),
|
|
|
|
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
|
2012-08-15 17:08:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
'model': fields.char('Related Document Model', size=128, select=1),
|
|
|
|
'res_id': fields.integer('Related Document ID', select=1),
|
|
|
|
'record_name': fields.function(get_record_name, type='string',
|
|
|
|
string='Message Record Name',
|
|
|
|
help="Name get of the related document."),
|
|
|
|
|
|
|
|
'subject': fields.char('Subject', size=128),
|
|
|
|
'date': fields.datetime('Date'),
|
|
|
|
|
|
|
|
# FP Note: do we need this ?
|
|
|
|
'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
|
|
|
|
|
|
|
|
# END FP Note
|
|
|
|
|
|
|
|
'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
|
|
|
|
'body': fields.text('Content', help="Content of Message", required=True),
|
|
|
|
|
2011-07-22 16:34:57 +00:00
|
|
|
}
|
2011-08-23 17:58:09 +00:00
|
|
|
_defaults = {
|
2012-04-03 17:34:49 +00:00
|
|
|
'type': 'email',
|
2012-08-15 17:08:22 +00:00
|
|
|
'date': (lambda *a: fields.datetime.now()),
|
2011-08-23 17:58:09 +00:00
|
|
|
}
|
2012-08-15 17:08:22 +00:00
|
|
|
|
2012-02-02 09:48:45 +00:00
|
|
|
#------------------------------------------------------
|
2012-06-25 13:42:53 +00:00
|
|
|
# Email api
|
2012-02-02 09:48:45 +00:00
|
|
|
#------------------------------------------------------
|
2012-08-15 17:08:22 +00:00
|
|
|
|
2011-07-22 16:34:57 +00:00
|
|
|
def init(self, cr):
|
|
|
|
cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
|
|
|
|
if not cr.fetchone():
|
|
|
|
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
|
|
|
|
|
2012-08-15 13:36:43 +00:00
|
|
|
def check(self, cr, uid, ids, mode, context=None):
|
2012-08-15 18:44:03 +00:00
|
|
|
"""
|
|
|
|
You can access a message if:
|
|
|
|
- you received it (a notification exists) or
|
|
|
|
- you can read the related document (res_model, res_id)
|
|
|
|
If a message is not attached to a document, normal access rights apply.
|
2012-05-08 13:56:00 +00:00
|
|
|
"""
|
|
|
|
if not ids:
|
|
|
|
return
|
|
|
|
res_ids = {}
|
|
|
|
if isinstance(ids, (int, long)):
|
|
|
|
ids = [ids]
|
|
|
|
cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
|
|
|
|
for rmod, rid in cr.fetchall():
|
|
|
|
if not (rmod and rid):
|
|
|
|
continue
|
|
|
|
res_ids.setdefault(rmod,set()).add(rid)
|
|
|
|
|
|
|
|
ima_obj = self.pool.get('ir.model.access')
|
|
|
|
for model, mids in res_ids.items():
|
|
|
|
# ignore mail messages that are not attached to a resource anymore when checking access rights
|
2012-07-06 09:41:41 +00:00
|
|
|
# (resource was deleted but message was not)
|
2012-05-08 13:56:00 +00:00
|
|
|
mids = self.pool.get(model).exists(cr, uid, mids)
|
|
|
|
ima_obj.check(cr, uid, model, mode)
|
|
|
|
self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
|
2012-08-15 17:08:22 +00:00
|
|
|
|
2012-05-08 13:56:00 +00:00
|
|
|
def create(self, cr, uid, values, context=None):
|
2012-08-15 13:36:43 +00:00
|
|
|
newid = super(mail_message, self).create(cr, uid, values, context)
|
|
|
|
self.check(cr, uid, [newid], mode='create', context=context)
|
|
|
|
|
|
|
|
# notify all followers
|
|
|
|
if values.get('model') and values.get('res_id'):
|
|
|
|
notification_obj = self.pool.get('mail.notification')
|
|
|
|
modobj = self.pool.get(values.get('model'))
|
|
|
|
follower_notify = []
|
2012-08-15 19:34:06 +00:00
|
|
|
for follower in modobj.message_follower_ids:
|
2012-08-15 13:36:43 +00:00
|
|
|
if follower.id <> uid:
|
|
|
|
follower_notify.append(follower.id)
|
|
|
|
self.pool.get('mail.notification').notify(cr, uid, follower_notify, newid, context=context)
|
|
|
|
return newid
|
2012-05-08 13:56:00 +00:00
|
|
|
|
|
|
|
def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
|
|
|
|
self.check(cr, uid, ids, 'read', context=context)
|
|
|
|
return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
|
|
|
|
|
2011-08-23 17:58:09 +00:00
|
|
|
def copy(self, cr, uid, id, default=None, context=None):
|
|
|
|
"""Overridden to avoid duplicating fields that are unique to each email"""
|
|
|
|
if default is None:
|
|
|
|
default = {}
|
2012-07-16 15:39:01 +00:00
|
|
|
self.check(cr, uid, [id], 'read', context=context)
|
2012-08-15 18:44:03 +00:00
|
|
|
default.update(message_id=False, headers=False)
|
2011-08-23 17:58:09 +00:00
|
|
|
return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
|
2012-08-15 17:08:22 +00:00
|
|
|
|
2012-05-08 13:56:00 +00:00
|
|
|
def write(self, cr, uid, ids, vals, context=None):
|
2012-08-15 13:36:43 +00:00
|
|
|
result = super(mail_message, self).write(cr, uid, ids, vals, context)
|
|
|
|
self.check(cr, uid, ids, 'write', context=context)
|
|
|
|
return result
|
2012-05-08 13:56:00 +00:00
|
|
|
|
|
|
|
def unlink(self, cr, uid, ids, context=None):
|
|
|
|
self.check(cr, uid, ids, 'unlink', context=context)
|
|
|
|
return super(mail_message, self).unlink(cr, uid, ids, context)
|
2011-08-23 17:58:09 +00:00
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
def parse_message(self, message, save_original=False, context=None):
|
2011-07-22 16:34:57 +00:00
|
|
|
"""Parses a string or email.message.Message representing an
|
|
|
|
RFC-2822 email, and returns a generic dict holding the
|
|
|
|
message details.
|
|
|
|
|
|
|
|
:param message: the message to parse
|
|
|
|
:type message: email.message.Message | string | unicode
|
2011-09-07 15:13:48 +00:00
|
|
|
:param bool save_original: whether the returned dict
|
|
|
|
should include an ``original`` entry with the base64
|
|
|
|
encoded source of the message.
|
2011-07-22 16:34:57 +00:00
|
|
|
:rtype: dict
|
|
|
|
:return: A dict with the following structure, where each
|
|
|
|
field may not be present if missing in original
|
|
|
|
message::
|
|
|
|
|
|
|
|
{ 'message-id': msg_id,
|
|
|
|
'subject': subject,
|
|
|
|
'from': from,
|
|
|
|
'to': to,
|
|
|
|
'cc': cc,
|
|
|
|
'headers' : { 'X-Mailer': mailer,
|
|
|
|
#.. all X- headers...
|
|
|
|
},
|
2012-04-20 09:36:45 +00:00
|
|
|
'content_subtype': msg_mime_subtype,
|
2011-07-22 18:23:07 +00:00
|
|
|
'body_text': plaintext_body
|
2011-07-22 16:34:57 +00:00
|
|
|
'body_html': html_body,
|
2011-10-18 03:39:13 +00:00
|
|
|
'attachments': [('file1', 'bytes'),
|
|
|
|
('file2', 'bytes') }
|
2011-07-22 16:34:57 +00:00
|
|
|
# ...
|
|
|
|
'original': source_of_email,
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
msg_txt = message
|
|
|
|
if isinstance(message, str):
|
|
|
|
msg_txt = email.message_from_string(message)
|
|
|
|
|
|
|
|
# Warning: message_from_string doesn't always work correctly on unicode,
|
|
|
|
# we must use utf-8 strings here :-(
|
|
|
|
if isinstance(message, unicode):
|
|
|
|
message = message.encode('utf-8')
|
|
|
|
msg_txt = email.message_from_string(message)
|
|
|
|
|
|
|
|
message_id = msg_txt.get('message-id', False)
|
|
|
|
msg = {}
|
|
|
|
|
2011-09-07 15:13:48 +00:00
|
|
|
if save_original:
|
|
|
|
# save original, we need to be able to read the original email sometimes
|
|
|
|
msg['original'] = message.as_string() if isinstance(message, Message) \
|
|
|
|
else message
|
|
|
|
msg['original'] = base64.b64encode(msg['original']) # binary fields are b64
|
2011-07-22 16:34:57 +00:00
|
|
|
|
|
|
|
if not message_id:
|
|
|
|
# Very unusual situation, be we should be fault-tolerant here
|
|
|
|
message_id = time.time()
|
|
|
|
msg_txt['message-id'] = message_id
|
|
|
|
_logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
msg_fields = msg_txt.keys()
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['id'] = message_id
|
|
|
|
msg['message-id'] = message_id
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'Subject' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['subject'] = decode(msg_txt.get('Subject'))
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'Content-Type' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['content-type'] = msg_txt.get('Content-Type')
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'From' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'To' in msg_fields:
|
2011-09-07 16:26:17 +00:00
|
|
|
msg['to'] = decode(msg_txt.get('To'))
|
2011-12-09 14:28:39 +00:00
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'Delivered-To' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['to'] = decode(msg_txt.get('Delivered-To'))
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'CC' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['cc'] = decode(msg_txt.get('CC'))
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'Cc' in msg_fields:
|
2011-12-09 14:28:39 +00:00
|
|
|
msg['cc'] = decode(msg_txt.get('Cc'))
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'Reply-To' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['reply'] = decode(msg_txt.get('Reply-To'))
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'Date' in msg_fields:
|
2011-08-09 23:44:28 +00:00
|
|
|
date_hdr = decode(msg_txt.get('Date'))
|
2012-05-10 13:54:47 +00:00
|
|
|
# convert from email timezone to server timezone
|
|
|
|
date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone()))
|
|
|
|
date_server_datetime_str = date_server_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
|
|
msg['date'] = date_server_datetime_str
|
2011-07-22 16:34:57 +00:00
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'Content-Transfer-Encoding' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'References' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['references'] = msg_txt.get('References')
|
|
|
|
|
2012-05-10 13:54:47 +00:00
|
|
|
if 'In-Reply-To' in msg_fields:
|
2011-07-22 16:34:57 +00:00
|
|
|
msg['in-reply-to'] = msg_txt.get('In-Reply-To')
|
|
|
|
|
|
|
|
msg['headers'] = {}
|
2012-04-20 09:36:45 +00:00
|
|
|
msg['content_subtype'] = 'plain'
|
2011-07-22 16:34:57 +00:00
|
|
|
for item in msg_txt.items():
|
|
|
|
if item[0].startswith('X-'):
|
|
|
|
msg['headers'].update({item[0]: item[1]})
|
|
|
|
if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
|
|
|
|
encoding = msg_txt.get_content_charset()
|
|
|
|
body = msg_txt.get_payload(decode=True)
|
|
|
|
if 'text/html' in msg.get('content-type', ''):
|
|
|
|
msg['body_html'] = body
|
2012-04-20 09:36:45 +00:00
|
|
|
msg['content_subtype'] = 'html'
|
2012-02-22 15:32:20 +00:00
|
|
|
if body:
|
2012-02-17 13:58:24 +00:00
|
|
|
body = tools.html2plaintext(body)
|
2011-07-22 18:23:07 +00:00
|
|
|
msg['body_text'] = tools.ustr(body, encoding)
|
2011-07-22 16:34:57 +00:00
|
|
|
|
2011-10-18 03:39:13 +00:00
|
|
|
attachments = []
|
2011-07-22 16:34:57 +00:00
|
|
|
if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
|
|
|
|
body = ""
|
|
|
|
if 'multipart/alternative' in msg.get('content-type', ''):
|
2012-04-20 09:36:45 +00:00
|
|
|
msg['content_subtype'] = 'alternative'
|
2011-07-22 16:34:57 +00:00
|
|
|
else:
|
2012-04-20 09:36:45 +00:00
|
|
|
msg['content_subtype'] = 'mixed'
|
2011-07-22 16:34:57 +00:00
|
|
|
for part in msg_txt.walk():
|
|
|
|
if part.get_content_maintype() == 'multipart':
|
|
|
|
continue
|
|
|
|
|
|
|
|
encoding = part.get_content_charset()
|
|
|
|
filename = part.get_filename()
|
|
|
|
if part.get_content_maintype()=='text':
|
|
|
|
content = part.get_payload(decode=True)
|
|
|
|
if filename:
|
2011-10-18 03:39:13 +00:00
|
|
|
attachments.append((filename, content))
|
2011-07-22 16:34:57 +00:00
|
|
|
content = tools.ustr(content, encoding)
|
|
|
|
if part.get_content_subtype() == 'html':
|
|
|
|
msg['body_html'] = content
|
2012-04-20 09:36:45 +00:00
|
|
|
msg['content_subtype'] = 'html' # html version prevails
|
2011-07-22 16:34:57 +00:00
|
|
|
body = tools.ustr(tools.html2plaintext(content))
|
2012-02-23 05:40:22 +00:00
|
|
|
body = body.replace(' ', '')
|
2011-07-22 16:34:57 +00:00
|
|
|
elif part.get_content_subtype() == 'plain':
|
|
|
|
body = content
|
|
|
|
elif part.get_content_maintype() in ('application', 'image'):
|
|
|
|
if filename :
|
2011-10-18 03:39:13 +00:00
|
|
|
attachments.append((filename,part.get_payload(decode=True)))
|
2011-07-22 16:34:57 +00:00
|
|
|
else:
|
|
|
|
res = part.get_payload(decode=True)
|
|
|
|
body += tools.ustr(res, encoding)
|
|
|
|
|
2011-07-22 18:23:07 +00:00
|
|
|
msg['body_text'] = body
|
2011-08-22 17:16:59 +00:00
|
|
|
msg['attachments'] = attachments
|
|
|
|
|
|
|
|
# for backwards compatibility:
|
|
|
|
msg['body'] = msg['body_text']
|
2012-04-20 09:36:45 +00:00
|
|
|
msg['sub_type'] = msg['content_subtype'] or 'plain'
|
2011-07-22 16:34:57 +00:00
|
|
|
return msg
|
|
|
|
|
2012-03-27 11:58:00 +00:00
|
|
|
|