blob: 938f358e266d53a573469873fff1e500e021a215 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
import HTMLParser
import re
from pylons import tmpl_context as c, app_globals as g, config
from bson import ObjectId
import markupsafe
from allura.lib import helpers as h
from allura.lib.decorators import task
from allura.lib import mail_util
from allura.lib import exceptions as exc
log = logging.getLogger(__name__)
smtp_client = mail_util.SMTPClient()
def mail_meta_content(metalink):
'''
Helper function used to include a view action button in your email client
https://developers.google.com/gmail/markup/reference/go-to-action#view_action
:param metalink: url to the page the action button links to
'''
return h.html.literal("""\
<div itemscope itemtype="http://schema.org/EmailMessage">
<div itemprop="action" itemscope itemtype="http://schema.org/ViewAction">
<link itemprop="url" href="%s"></link>
<meta itemprop="name" content="View"></meta>
</div>
<meta itemprop="description" content="View"></meta>
</div>""" % metalink)
@task
def route_email(
peer, mailfrom, rcpttos, data):
'''
Route messages according to their destination:
<topic>@<mount_point>.<subproj2>.<subproj1>.<project>.projects.domain.net
gets sent to c.app.handle_message(topic, message)
'''
try:
msg = mail_util.parse_message(data)
except: # pragma no cover
log.exception('Parse Error: (%r,%r,%r)', peer, mailfrom, rcpttos)
return
if mail_util.is_autoreply(msg):
log.info('Skipping autoreply message: %s', msg['headers'])
return
mail_user = mail_util.identify_sender(peer, mailfrom, msg['headers'], msg)
with h.push_config(c, user=mail_user):
log.info('Received email from %s', c.user.username)
# For each of the addrs, determine the project/app and route
# appropriately
for addr in rcpttos:
try:
userpart, project, app = mail_util.parse_address(addr)
with h.push_config(c, project=project, app=app):
if not app.has_access(c.user, userpart):
log.info('Access denied for %s to mailbox %s',
c.user, userpart)
elif not c.app.config.options.get('AllowEmailPosting', True):
log.info("Posting from email is not enabled")
else:
if msg['multipart']:
msg_hdrs = msg['headers']
for part in msg['parts']:
if part.get('content_type', '').startswith('multipart/'):
continue
msg = dict(
headers=dict(msg_hdrs, **part['headers']),
message_id=part['message_id'],
in_reply_to=part['in_reply_to'],
references=part['references'],
filename=part['filename'],
content_type=part['content_type'],
payload=part['payload'])
c.app.handle_message(userpart, msg)
else:
c.app.handle_message(userpart, msg)
except exc.MailError, e:
log.error('Error routing email to %s: %s', addr, e)
except:
log.exception('Error routing mail to %s', addr)
def create_multipart_msg(text, metalink=None):
"""
Convert Markdown text to plaintext & HTML, combine into a multipart email Message
:param text:
:param metalink:
:return:
"""
def replace_html(matchobj):
text_within_div = matchobj.group(1)
text_within_div = text_within_div.replace('</p>', '\n')
text_within_div = markupsafe._striptags_re.sub('', text_within_div)
return text_within_div
plain_text = text
plain_text = re.sub(r'<div class="markdown_content">(.*)</div>', # strip HTML from markdown generated blocks
replace_html,
plain_text,
flags=re.DOTALL, # match newlines too
)
plain_text = HTMLParser.HTMLParser().unescape(plain_text) # put literal HTML tags back into plaintext
plain_msg = mail_util.encode_email_part(plain_text, 'plain')
html_text = g.forge_markdown(email=True).convert(text)
if metalink:
html_text = html_text + mail_meta_content(metalink)
html_msg = mail_util.encode_email_part(html_text, 'html')
multi_msg = mail_util.make_multipart_message(plain_msg, html_msg)
return multi_msg, plain_msg
@task
def sendmail(fromaddr, destinations, text, reply_to, subject,
message_id, in_reply_to=None, sender=None, references=None, metalink=None):
'''
Send an email to the specified list of destinations with respect to the preferred email format specified by user.
It is best for broadcast messages.
:param fromaddr: ObjectId or str(ObjectId) of user, or email address str
'''
from allura import model as M
addrs_plain = []
addrs_multi = []
if fromaddr is None:
fromaddr = g.noreply
elif not isinstance(fromaddr, basestring) or '@' not in fromaddr:
log.warning('Looking up user with fromaddr: %s', fromaddr)
user = M.User.query.get(_id=ObjectId(fromaddr), disabled=False, pending=False)
if not user:
log.warning('Cannot find user with ID: %s', fromaddr)
fromaddr = g.noreply
else:
fromaddr = user.email_address_header()
# Divide addresses based on preferred email formats
for addr in destinations:
if mail_util.isvalid(addr):
addrs_plain.append(addr)
else:
try:
user = M.User.query.get(_id=ObjectId(addr), disabled=False, pending=False)
if not user:
log.warning('Cannot find user with ID: %s', addr)
continue
except:
log.exception('Error looking up user with ID: %r' % addr)
continue
addr = user.email_address_header()
if not addr and user.email_addresses:
addr = user.email_addresses[0]
log.warning(
'User %s has not set primary email address, using %s',
user._id, addr)
if not addr:
log.error(
"User %s (%s) has not set any email address, can't deliver",
user._id, user.username)
continue
if user.get_pref('email_format') == 'plain':
addrs_plain.append(addr)
else:
addrs_multi.append(addr)
multi_msg, plain_msg = create_multipart_msg(text, metalink)
smtp_client.sendmail(
addrs_multi, fromaddr, reply_to, subject, message_id,
in_reply_to, multi_msg, sender=sender, references=references)
smtp_client.sendmail(
addrs_plain, fromaddr, reply_to, subject, message_id,
in_reply_to, plain_msg, sender=sender, references=references)
@task
def sendsimplemail(
fromaddr,
toaddr,
text,
reply_to,
subject,
message_id,
in_reply_to=None,
sender=None,
references=None,
cc=None):
'''
Send a single mail to the specified address.
It is best for single user notifications.
:param fromaddr: ObjectId or str(ObjectId) of user, or email address str
:param toaddr: ObjectId or str(ObjectId) of user, or email address str
'''
from allura import model as M
if fromaddr is None:
fromaddr = g.noreply
elif not isinstance(fromaddr, basestring) or '@' not in fromaddr:
log.warning('Looking up user with fromaddr: %s', fromaddr)
user = M.User.query.get(_id=ObjectId(fromaddr), disabled=False, pending=False)
if not user:
log.warning('Cannot find user with ID: %s', fromaddr)
fromaddr = g.noreply
else:
fromaddr = user.email_address_header()
if not isinstance(toaddr, basestring) or '@' not in toaddr:
log.warning('Looking up user with toaddr: %s', toaddr)
user = M.User.query.get(_id=ObjectId(toaddr), disabled=False, pending=False)
if not user:
log.warning('Cannot find user with ID: %s', toaddr)
toaddr = g.noreply
else:
toaddr = user.email_address_header()
multi_msg, plain_msg = create_multipart_msg(text)
smtp_client.sendmail(
[toaddr], fromaddr, reply_to, subject, message_id,
in_reply_to, multi_msg, sender=sender, references=references, cc=cc, to=toaddr)
def send_system_mail_to_user(user_or_emailaddr, subject, text):
'''
Sends a standard email from the Allura system itself, to a user.
This is a helper function around sendsimplemail() that generates a new task
:param user_or_emailaddr: an email address (str) or a User object
:param subject: subject of the email
:param text: text of the email (markdown)
'''
if isinstance(user_or_emailaddr, basestring):
toaddr = user_or_emailaddr
else:
toaddr = user_or_emailaddr._id
email = {
'toaddr': toaddr,
'fromaddr': u'"{}" <{}>'.format(
config['site_name'],
config['forgemail.return_path']
),
'sender': unicode(config['forgemail.return_path']),
'reply_to': unicode(config['forgemail.return_path']),
'message_id': h.gen_message_id(),
'subject': subject,
'text': text,
}
sendsimplemail.post(**email)