Python 3.10 is now available in preview.

Mail API for Python 3

This page describes how to use the Mail API, one of the legacy bundled services, with the Python 3 runtime for the standard environment. Your app can access the bundled services through the App Engine services SDK for Python 3.

Overview

In Python 3, the mail handling functionality is included in the google.appengine.api.mail module. This is different from Python 2, where the mail_handlers module was provided by webapp. The Mail API for Python 3 can be used to receive emails and bounce notifications.

Using the Mail API

Your app receives mail when an email is sent as the request body in an HTTP POST request. For the app to handle incoming emails, the app needs to match the URL with the path /_ah/mail/[ADDRESS]. The [ADDRESS] part of the path is usually an email address with the suffix @<Cloud-Project-ID>.appspotmail.com. Emails sent to the app in this format will be routed to the function.

Python 3 doesn't require the app to specify a handler script in the app.yaml file, so you can remove all handler sections in app.yaml.

Your app.yaml file should keep the following lines:

inbound_services:
- mail
- mail_bounce

Sending mail

You do not need to make changes to your app's configuration when upgrading to Python 3. The behavior, features, and setup instructions for sending mail remains the same. Refer to the following guides for more details:

Receiving mail

To receive mail, you need to import the google.appengine.api.mail module and use the InboundEmailMessage class to represent an email. This class needs to be instantiated to retrieve the email content from the incoming HTTP request.

Previously in Python 2, apps could access the InboundEmailMessage class by overriding the receive() method in the webapp handler InboundEmailHandler. This is not needed in Python 3; instead, the app needs to instantiate a new object.

Web frameworks

When using Python web frameworks, the InboundEmailMessage constructor takes in the bytes of the HTTP request body. There are multiple ways to create the InboundEmailMessage object in Python 3. The following are examples for Flask and Djago apps:

Python 3 (Flask)

In Flask, request.get_data() gives the request bytes.

from flask import request

@app.route('/_ah/mail/<address>', methods=['POST'])
def receive_mail(address):
     mail_message = mail.InboundEmailMessage(request.get_data())

     # Do something with the message
     print('Received greeting at %s from %s: %s' % (
         mail_message.to,
         mail_message.sender,
         mail_message.bodies('text/plain')))

     return 'Success'

Python 3 (Django)

In Django, request.body gives the bytes of the HTTP request body.

def receive_mail(request):
     mail_message = mail.InboundEmailMessage(request.body)
     logging.info('Received greeting from %s: %s' % (mail_message.sender, mail_message.body))
     return HttpResponse('Success.')

urlpatterns = (
   url(r'_ah/mail/.*$', receive_mail),
)

For the entire code sample, select appengine-python3-bundled-services.zip from here and click DOWNLOAD.

Other WSGI-compliant frameworks

For other WSGI-compliant frameworks, we recommend using the same method as the Flask and Django examples to create the InboundEmailMessage. This method works when the bytes of the HTTP request body are directly available.

WSGI app without a web framework

If your app is a WSGI app that does not use a web framework, it is possible that the bytes of the HTTP request body is not directly available. If the bytes of the HTTP request body are directly available, we recommend that you use a Python web framework.

In Python 3, a factory method named from_environ is defined for InboundEmailMessage. This method is a class method that takes the WSGI environ dictionary as the input, and can be used for any WSGI application.

In the following example, notice how environ is taken in as an input to get the mail_message:

Python 3 (WSGI app)

def HelloReceiver(environ, start_response):
    if environ['REQUEST_METHOD'] != 'POST':
        return ('', http.HTTPStatus.METHOD_NOT_ALLOWED, [('Allow', 'POST')])

    mail_message = mail.InboundEmailMessage.from_environ(environ)

    # Add logic for what to do with the email
    print('Received greeting from %s: %s' % (mail_message.sender, mail_message.body))

    # return suitable response
    response = http.HTTPStatus.OK
    start_response(f'{response.value} {response.phrase}', [])
    return ['success'.encode('utf-8')]

routes = {
           MAIL_HANDLER_URL_PATTERN: HelloReceiver
        }

class WSGIApplication():
    def __call__(self, environ, start_response):
        path = environ.get('PATH_INFO', '')
        for regex, callable in routes.items():
            match = re.search(regex, path)
            if match is not None:
                return callable(environ, start_response)

Receiving bounce notifications

A bounce notification is an automated message from an email system that indicates a problem with your app's message delivery. To process bounce notifications, your app needs to match incoming URL paths with the /_ah/bounce path.

Like InboundEmailMessage, the BounceNotification class for Python 2 was accessible by overriding the receive() method in the webapp handler BounceNotificationHandler.

In Python 3, the app needs to instantiate the BounceNotification object, which can be created in multiple ways depending on the Python web framework used.

Web frameworks

The BounceNotification object is initialized with the values that are retrieved by calling post_vars.get(key).

When using a Python web framework, like Flask or Django, the BounceNotification constructor takes in a dictionary named post_vars, which contains the POST request of the form data. To retrieve the data, the method get() needs to be defined on the input object. The key is a list of input values that can be read and retrieved by BounceNotification and can be any of the following:

original-to, original-cc, original-bcc, original-subject, original-text, notification-from, notification-to, notification-cc, notification-bcc, notification-subject, notification-text, raw-message

In most web frameworks, this data is available as a multi dictionary in the request object. Most of these types can be converted into a dictionary keyed by strings.

For all keys except raw-message, the value can be anything. Usually, the value is either a single value such as a string, or a list of values, such as {'to': ['bob@abc.com', 'alice@abc.com']}. The default value for all fields is an empty string. These values will populate the original and notification properties.

For the raw-message key, the value needs to be a valid input to the constructor of EmailMessage. It can either be a single value or a single valued list. The raw-message key is used to initialize the original_raw_message property of the object.

Python 2 (webapp2)

class LogBounceHandler(BounceNotificationHandler):
    def receive(self, bounce_message):
        logging.info('Received bounce post ... [%s]', self.request)
        logging.info('Bounce original: %s', bounce_message.original)
        logging.info('Bounce notification: %s', bounce_message.notification)

Python 3 (Flask)

In Flask, request.form of type werkzeug.datastructures.MultiDict gives the POST variables. However, the get() method for this type only returns a value, even if there are multiple values for the key is present.

To get all values corresponding to a key, the app needs to call dict(request.form.list()), which results in a dictionary where each value is a list.

@app.route("/_ah/bounce")
def receive_bounce():
    """Handles Bounce notifications received."""
    bounce_message = mail.BounceNotification(request.form.lists())

    print("Bounce notification: ", bounce_message.__notification)
    print("Bounce notification- original message: ", bounce_message.__original)
    return "success"

Python 3 (Django)

In Django, request.POST of type django.http.QueryDict gives the POST variables. However, the get() method for this type only returns a value, even if there are multiple values for the key is present.

To get all values corresponding to a key, the app needs to call dict(request.form.list()), which results in a dictionary where each value is a list.

def receive_bounce(request):
    bounce_message = mail.BounceNotification(dict(request.POST.lists())

    # Make a simple text log
    logger.log_text('Received bounce notification: %s' % (bounce_message.__notification))
    logger.log_text('Received bounce notification for the original message: %s' % (
        bounce_message.__original))
    return HttpResponse('Success')

WSGI app without a web framework

If your app is a WSGI app that does not use a web framework, it is possible that the form variables of the HTTP POST request are not directly available in a dictionary.

In Python 3, a factory method named from_environ is defined for BounceNotification. This method is a class method that takes the WSGI environ dictionary as the input, and can be used for any WSGI application.

In the following example, notice how environ is taken in as an input to get the bounce_message:

Python 3

def BounceReceiver(environ, start_response):
    if environ['REQUEST_METHOD'] != 'POST':
        return ('', http.HTTPStatus.METHOD_NOT_ALLOWED, [('Allow', 'POST')])

    bounce_message = mail.BounceNotification.from_environ(environ)
    # Do something with the message
    print('Received bounce post.')
    print('Bounce original: %s', bounce_message.original)
    print('Bounce notification: %s', bounce_message.notification)

    # Return suitable response
    response = http.HTTPStatus.OK
    start_response(f'{response.value} {response.phrase}', [])
    return ['success'.encode('utf-8')]

Code samples

To view the complete code samples from this guide, select appengine-python3-bundled-services.zip from here and click DOWNLOAD.