[MERGE][bzr-email] Implement mail sending using smtplib

Marius Gedminas marius at pov.lt
Fri Jan 26 14:24:51 GMT 2007


On Thu, Jan 25, 2007 at 05:38:15PM -0600, John Arbash Meinel wrote:
> Then I implemented an SMTPConnection class, which has a little bit of
> knowledge about what configuration parameters bzrlib should use to
> define an SMTP connection (smtp_server, smtp_username, smtp_password).
> 
> That part was easy. The tricky part was to make everything support
> Unicode. And support it in a way that wouldn't confuse my SMTP server,
> and would look nice in Thunderbird.

Ah, yes.  Been there, done that
(http://mg.pov.lt/blog/unicode-emails-in-python.html).

> # Bazaar revision bundle v0.8
> #
> # message:
> #   Update the README
> # committer: John Arbash Meinel <john at arbash-meinel.com>
> # date: Thu 2007-01-25 17:04:53.723999977 -0600
> 
> === added file emailer.py // file-id:emailer.py-20070123220937-ec5y2n2oeoa0p4ue
> ... -1 // last-changed:john at arbash-meinel.com-20070125220812-yceplg1ck4qt85la
> --- /dev/null
> +++ emailer.py
...
> +class EmailSender(object):
...
> +    def body(self):
...
> +        # We must use StringIO.StringIO because we want a Unicode string
> +        from StringIO import StringIO
> +        outf = StringIO()
...
> +
> +    def get_diff(self):
...
> +
> +        # We want a cStringIO because we want an 8-bit string

StringIO.StringIO can handle both 8-bit strings and Unicode strings.
(It cannot handle mixed content, though).  Perhaps rephrase the comment
to

           # We can use cStringIO because we have an 8-bit string

?

Or is there some type-based dispatching in bzrlib that chooses Unicode
versus 8-bit from the file object?

> +        from cStringIO import StringIO
> +        diff_content = StringIO()
> +        show_diff_trees(tree_old, tree_new, diff_content)
> +        lines = diff_content.getvalue().split("\n")
> +        numlines = len(lines)

It would be more efficient to do

           numlines = diff_content.getvalue().count("\n") + 1

since you don't use ``lines`` for anything else.

> +        if numlines <= difflimit:
> +            return diff_content.getvalue()
> +        else:
> +            return ("\nDiff too large for email"
> +                    " (%d lines, the limit is %d).\n"
> +                    % (numlines, difflimit))
> +
> +    def difflimit(self):
> +        """maximum number of lines of diff to show."""

Capitalize "Maximum"?

...
> +    def _send_using_process(self):
> +        """Spawn a 'mail' subprocess to send the email."""
> +        # TODO think up a good test for this, but I think it needs
> +        # a custom binary shipped with. RBC 20051021

Why a custom binary?  You could set post_commit_mailer to the name of a
test-helper Python script that stores its arguments and stdin somewhere
on the disk.  Perhaps copy the test script into a temporary directory,
and make it write its result to os.path.join(os.path.dirname(sys.argv[0]),
'out.txt').

> +        try:
> +            process = subprocess.Popen(self._command_line(),
> +                                       stdin=subprocess.PIPE)
> +            try:
> +                message = self.body().encode('utf8') + self.get_diff()
> +                result = process.communicate(message)[0]
...

> === added file smtp_connection.py // file-id:smtp_connection.py-20070125220755-
> ... k6ueimjqwn16wvr9-1 // last-changed:john at arbash-meinel.com-20070125225439-5b
> ... trv82m7dhekt8z
> --- /dev/null
> +++ smtp_connection.py
...
> +class SMTPConnection(object):
...
> +    def _basic_message(self, from_address, to_addresses, subject):
> +        """Create the basic Message using the right Header info.
> +
> +        This creates an email Message with no payload.
> +        :param from_address: The Unicode from address.
> +        :param to_addresses: A list of Unicode destination addresses.
> +        :param subject: A Unicode subject for the email.
> +        """
> +        # It would be nice to use a single part if we only had one, but we
> +        # would have to know ahead of time how many parts we needed.
> +        # So instead, just default to multipart.
> +        msg = MIMEMultipart()
> +
> +        # Header() does a good job of doing the proper encoding. However it
> +        # confuses my SMTP server because it doesn't decode the strings. So it
> +        # is better to send the addresses as:
> +        #   =?utf-8?q?username?= <email at addr.com>
> +        # Which is how Thunderbird does it

I think encoding the <email at addr.com> part is actually a violation of RFC-2822.
The stdlib email package is quirky.

> +        from_user, from_email = self._split_address(from_address)
> +        msg['From'] = '%s <%s>' % (Header(from_user, 'utf8'), from_email)

If you use Header(some_str, 'utf8'), then it will *always* use the
=?utf-8?..?= quoting (at least it acts this way in Python 2.4).  This is
redundant for names that can be expressed in plain text.
Header(some_unicode) does the quoting only when necessary.  The type of
the string is important, the email package treats 'foo' and u'foo'
differently.

> +        msg['User-Agent'] = 'bzr/%s' % _bzrlib_version
> +
> +        to_emails = []
> +        to_header = []
> +        for addr in to_addresses:
> +            to_user, to_email = self._split_address(addr)
> +            to_emails.append(to_email)
> +            to_header.append('%s <%s>' % (Header(to_user, 'utf8'), to_email))
> +
> +        msg['To'] = ', '.join(to_header)
> +        msg['Subject'] = Header(subject)
> +        return msg, from_email, to_emails
...

> === added file tests/test_smtp_connection.py // file-id:test_smtp_connection-20
> ... 070125220755-k6ueimjqwn16wvr9-2 // last-changed:john at arbash-meinel.com-2007
> ... 0125225439-5btrv82m7dhekt8z
> --- /dev/null
> +++ tests/test_smtp_connection.py
...
> +class InstrumentedSMTPConnection(SMTPConnection):
> +    """Instrument SMTPConnection.
> +
> +    We don't want to actually connect or send messages, so this just
> +    fakes it.
> +    """
> +    

What does bzr's coding style say about trailing whitespace?

...
> +class TestSMTPConnection(TestCase):
> +
> +    def get_connection(self, text):
> +        my_config = config.GlobalConfig()
> +        config_file = StringIO(text)
> +        (my_config._get_parser(config_file))

Why the parentheses around it?

> +        return InstrumentedSMTPConnection(my_config)
...

> === modified file README
> --- README
> +++ README
...
> +Also, if using 'smtplib', the messages will be sent as a utf-8 text message,

Some pedantic people insist that UTF-8 be spelled "UTF-8", with all the
capital letters, as it is an abbreviation for UCS (or Unicode)
Transformation Format.

> +with a 8-bit text diff attached (rather than all-as-one). Work has also been
> +done to make sure usernames do not have to be ascii.
> +
...

Marius Gedminas
-- 
CBQ is merely the oldest kid on the block - yet it is by far the least useful
qdisc and also the most complex one. I advise *against* using it. This may come
as something of a shock to many who fell for the 'sendmail effect', which
learns us that any complex technology which doesn't come with documentation
must be the best available.
		-- Linux Advanced Routing HOWTO
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 189 bytes
Desc: Digital signature
Url : https://lists.ubuntu.com/archives/bazaar/attachments/20070126/b8b2a14b/attachment.pgp 


More information about the bazaar mailing list