| # 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 re |
| import logging |
| import smtplib |
| import email.feedparser |
| from email.MIMEMultipart import MIMEMultipart |
| from email.MIMEText import MIMEText |
| from email import header |
| |
| import tg |
| from paste.deploy.converters import asbool, asint |
| from formencode import validators as fev |
| from pylons import tmpl_context as c |
| |
| from allura.lib.utils import ConfigProxy |
| from allura.lib import exceptions as exc |
| from allura.lib import helpers as h |
| |
| log = logging.getLogger(__name__) |
| |
| RE_MESSAGE_ID = re.compile(r'<(.*)>') |
| config = ConfigProxy( |
| common_suffix='forgemail.domain', |
| return_path='forgemail.return_path') |
| EMAIL_VALIDATOR=fev.Email(not_empty=True) |
| |
| def Header(text, *more_text): |
| '''Helper to make sure we encode headers properly''' |
| if isinstance(text, header.Header): |
| return text |
| # email.header.Header handles str vs unicode differently |
| # see http://docs.python.org/library/email.header.html#email.header.Header.append |
| if type(text) != unicode: |
| raise TypeError('This must be unicode: %r' % text) |
| head = header.Header(text) |
| for m in more_text: |
| if type(m) != unicode: |
| raise TypeError('This must be unicode: %r' % text) |
| head.append(m) |
| return head |
| |
| def AddrHeader(fromaddr): |
| '''Accepts any of: |
| Header() instance |
| foo@bar.com |
| "Foo Bar" <foo@bar.com> |
| ''' |
| if isinstance(fromaddr, basestring) and ' <' in fromaddr: |
| name, addr = fromaddr.rsplit(' <', 1) |
| addr = '<' + addr # restore the char we just split off |
| addrheader = Header(name, addr) |
| if str(addrheader).startswith('=?'): # encoding escape chars |
| # then quoting the name is no longer necessary |
| name = name.strip('"') |
| addrheader = Header(name, addr) |
| else: |
| addrheader = Header(fromaddr) |
| return addrheader |
| |
| |
| def parse_address(addr): |
| userpart, domain = addr.split('@') |
| # remove common domain suffix |
| if not domain.endswith(config.common_suffix): |
| raise exc.AddressException, 'Unknown domain: ' + domain |
| domain = domain[:-len(config.common_suffix)] |
| path = '/'.join(reversed(domain.split('.'))) |
| project, mount_point = h.find_project('/' + path) |
| if project is None: |
| raise exc.AddressException, 'Unknown project: ' + domain |
| if len(mount_point) != 1: |
| raise exc.AddressException, 'Unknown tool: ' + domain |
| with h.push_config(c, project=project): |
| app = project.app_instance(mount_point[0]) |
| if not app: |
| raise exc.AddressException, 'Unknown tool: ' + domain |
| return userpart, project, app |
| |
| def parse_message(data): |
| # Parse the email to its constituent parts |
| parser = email.feedparser.FeedParser() |
| parser.feed(data) |
| msg = parser.close() |
| # Extract relevant data |
| result = {} |
| result['multipart'] = multipart = msg.is_multipart() |
| result['headers'] = dict(msg) |
| result['message_id'] = _parse_message_id(msg.get('Message-ID')) |
| result['in_reply_to'] = _parse_message_id(msg.get('In-Reply-To')) |
| result['references'] = _parse_message_id(msg.get('References')) |
| if result['message_id'] == []: |
| result['message_id'] = h.gen_message_id() |
| else: |
| result['message_id'] = result['message_id'][0] |
| if multipart: |
| result['parts'] = [] |
| for part in msg.walk(): |
| dpart = dict( |
| headers=dict(part), |
| message_id=result['message_id'], |
| in_reply_to=result['in_reply_to'], |
| references=result['references'], |
| content_type=part.get_content_type(), |
| filename=part.get_filename(None), |
| payload=part.get_payload(decode=True)) |
| charset = part.get_content_charset() |
| if charset: |
| dpart['payload'] = dpart['payload'].decode(charset) |
| result['parts'].append(dpart) |
| else: |
| result['payload'] = msg.get_payload(decode=True) |
| charset = msg.get_content_charset() |
| if charset: |
| result['payload'] = result['payload'].decode(charset) |
| return result |
| |
| def identify_sender(peer, email_address, headers, msg): |
| from allura import model as M |
| # Dumb ID -- just look for email address claimed by a particular user |
| addr = M.EmailAddress.query.get(_id=M.EmailAddress.canonical(email_address)) |
| if addr and addr.claimed_by_user_id: |
| return addr.claimed_by_user() |
| addr = M.EmailAddress.query.get(_id=M.EmailAddress.canonical(headers.get('From', ''))) |
| if addr and addr.claimed_by_user_id: |
| return addr.claimed_by_user() |
| return M.User.anonymous() |
| |
| def encode_email_part(content, content_type): |
| try: |
| return MIMEText(content.encode('ascii'), content_type, 'ascii') |
| except: |
| return MIMEText(content.encode('utf-8'), content_type, 'utf-8') |
| |
| def make_multipart_message(*parts): |
| msg = MIMEMultipart('related') |
| msg.preamble = 'This is a multi-part message in MIME format.' |
| alt = MIMEMultipart('alternative') |
| msg.attach(alt) |
| for part in parts: |
| alt.attach(part) |
| return msg |
| |
| def _parse_message_id(msgid): |
| if msgid is None: return [] |
| return [ mo.group(1) |
| for mo in RE_MESSAGE_ID.finditer(msgid) ] |
| |
| def _parse_smtp_addr(addr): |
| addr = str(addr) |
| addrs = _parse_message_id(addr) |
| if addrs and addrs[0]: return addrs[0] |
| if '@' in addr: return addr |
| return u'noreply@in.sf.net' |
| |
| def isvalid(addr): |
| '''return True if addr is a (possibly) valid email address, false |
| otherwise''' |
| try: |
| EMAIL_VALIDATOR.to_python(addr, None) |
| return True |
| except fev.Invalid: |
| return False |
| |
| class SMTPClient(object): |
| |
| def __init__(self): |
| self._client = None |
| |
| def sendmail(self, addrs, fromaddr, reply_to, subject, message_id, in_reply_to, message): |
| if not addrs: return |
| # We send one message with multiple envelope recipients, so use a generic To: addr |
| # It might be nice to refactor to send one message per recipient, and use the actual To: addr |
| message['To'] = Header(reply_to) |
| message['From'] = AddrHeader(fromaddr) |
| message['Reply-To'] = Header(reply_to) |
| message['Subject'] = Header(subject) |
| message['Message-ID'] = Header('<' + message_id + u'>') |
| if in_reply_to: |
| if not isinstance(in_reply_to, basestring): |
| raise TypeError('Only strings are supported now, not lists') |
| message['In-Reply-To'] = Header(u'<%s>' % in_reply_to) |
| content = message.as_string() |
| smtp_addrs = map(_parse_smtp_addr, addrs) |
| smtp_addrs = [ a for a in smtp_addrs if isvalid(a) ] |
| if not smtp_addrs: |
| log.warning('No valid addrs in %s, so not sending mail', |
| map(unicode, addrs)) |
| return |
| try: |
| self._client.sendmail( |
| config.return_path, |
| smtp_addrs, |
| content) |
| except: |
| self._connect() |
| self._client.sendmail( |
| config.return_path, |
| smtp_addrs, |
| content) |
| |
| def _connect(self): |
| if asbool(tg.config.get('smtp_ssl', False)): |
| smtp_client = smtplib.SMTP_SSL( |
| tg.config.get('smtp_server', 'localhost'), |
| asint(tg.config.get('smtp_port', 25)), |
| timeout=float(tg.config.get('smtp_timeout', 10)), |
| ) |
| else: |
| smtp_client = smtplib.SMTP( |
| tg.config.get('smtp_server', 'localhost'), |
| asint(tg.config.get('smtp_port', 465)), |
| timeout=float(tg.config.get('smtp_timeout', 10)), |
| ) |
| if tg.config.get('smtp_user', None): |
| smtp_client.login(tg.config['smtp_user'], tg.config['smtp_password']) |
| if asbool(tg.config.get('smtp_tls', False)): |
| smtp_client.starttls() |
| self._client = smtp_client |