From c8ca418cb0ce43c84e1cf2db6e3ea83cf250b12f Mon Sep 17 00:00:00 2001 From: Jeffrey Rennie Date: Tue, 26 Apr 2016 12:44:50 -0700 Subject: [PATCH] Add mail samples. For https://cloud.google.com/appengine/docs/python/mail/ --- appengine/__init__.py | 0 appengine/mail/README.md | 7 ++ appengine/mail/app.yaml | 60 +++++++++++ appengine/mail/attachment.py | 48 +++++++++ appengine/mail/attachment_test.py | 25 +++++ appengine/mail/handle_bounced_email.py | 28 +++++ appengine/mail/handle_bounced_email_test.py | 23 ++++ appengine/mail/handle_catchall.py | 16 +++ appengine/mail/handle_incoming_email.py | 41 ++++++++ appengine/mail/handle_incoming_email_test.py | 27 +++++ appengine/mail/handle_owner.py | 16 +++ appengine/mail/handle_support.py | 16 +++ appengine/mail/header.py | 54 ++++++++++ appengine/mail/header_test.py | 25 +++++ appengine/mail/index.html | 29 ++++++ appengine/mail/send_mail.py | 48 +++++++++ appengine/mail/send_mail_test.py | 24 +++++ appengine/mail/send_message.py | 51 +++++++++ appengine/mail/send_message_test.py | 24 +++++ appengine/mail/user_signup.py | 104 +++++++++++++++++++ appengine/mail/user_signup_test.py | 37 +++++++ 21 files changed, 703 insertions(+) create mode 100644 appengine/__init__.py create mode 100644 appengine/mail/README.md create mode 100644 appengine/mail/app.yaml create mode 100644 appengine/mail/attachment.py create mode 100644 appengine/mail/attachment_test.py create mode 100644 appengine/mail/handle_bounced_email.py create mode 100644 appengine/mail/handle_bounced_email_test.py create mode 100644 appengine/mail/handle_catchall.py create mode 100644 appengine/mail/handle_incoming_email.py create mode 100644 appengine/mail/handle_incoming_email_test.py create mode 100644 appengine/mail/handle_owner.py create mode 100644 appengine/mail/handle_support.py create mode 100644 appengine/mail/header.py create mode 100644 appengine/mail/header_test.py create mode 100644 appengine/mail/index.html create mode 100644 appengine/mail/send_mail.py create mode 100644 appengine/mail/send_mail_test.py create mode 100644 appengine/mail/send_message.py create mode 100644 appengine/mail/send_message_test.py create mode 100644 appengine/mail/user_signup.py create mode 100644 appengine/mail/user_signup_test.py diff --git a/appengine/__init__.py b/appengine/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/mail/README.md b/appengine/mail/README.md new file mode 100644 index 00000000000..5ec82e066f7 --- /dev/null +++ b/appengine/mail/README.md @@ -0,0 +1,7 @@ +## App Engine Email Docs Snippets + +This sample application demonstrates different ways to send and receive email +on App Engine + + + diff --git a/appengine/mail/app.yaml b/appengine/mail/app.yaml new file mode 100644 index 00000000000..5b8e13f50fc --- /dev/null +++ b/appengine/mail/app.yaml @@ -0,0 +1,60 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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. +runtime: python27 +api_version: 1 +threadsafe: yes + +# [START bounce_service] +# [START mail_service] +inbound_services: +- mail +# [END mail_service] +- mail_bounce +# [END bounce_service] + +handlers: +- url: /user/.+ + script: user_signup.app +- url: /send_mail + script: send_mail.app +- url: /send_message + script: send_message.app +# [START handle_incoming_email] +- url: /_ah/mail/.+ + script: handle_incoming_email.app + login: admin +# [END handle_incoming_email] +# [START handle_all_email] +- url: /_ah/mail/owner@.*your_app_id\.appspotmail\.com + script: handle_owner.app + login: admin +- url: /_ah/mail/support@.*your_app_id\.appspotmail\.com + script: handle_support.app + login: admin +- url: /_ah/mail/.+ + script: handle_catchall.app + login: admin +# [END handle_all_email] +# [START handle_bounced_email] +- url: /_ah/bounce + script: handle_bounced_email.app + login: admin +# [END handle_bounced_email] +- url: /attachment + script: attachment.app +- url: /header + script: header.app +- url: / + static_files: index.html + upload: index.html \ No newline at end of file diff --git a/appengine/mail/attachment.py b/appengine/mail/attachment.py new file mode 100644 index 00000000000..9e3d431f829 --- /dev/null +++ b/appengine/mail/attachment.py @@ -0,0 +1,48 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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. +from google.appengine.api import app_identity +from google.appengine.api import mail +import webapp2 + + +# [START send_attachment] +class AttachmentHandler(webapp2.RequestHandler): + def post(self): + f = self.request.POST['file'] + mail.send_mail(sender=('%s@appspot.gserviceaccount.com' % + app_identity.get_application_id()), + to="Albert Johnson ", + subject="The doc you requested", + body="""Attached is the document file you requested. + +The example.com Team +""", + attachments=[(f.filename, f.file.read())]) +# [END send_attachment] + self.response.content_type = 'text/plain' + self.response.write('Sent %s to Albert.' % f.filename) + + def get(self): + self.response.content_type = 'text/html' + self.response.write(""" +
+ Send a file to Albert:
+

+ +
+ Enter an email thread id: + +
""") + + def post(self): + print repr(self.request.POST) + id = self.request.POST['thread_id'] + send_example_mail('%s@appspot.gserviceaccount.com' % + app_identity.get_application_id(), id) + self.response.content_type = 'text/plain' + self.response.write( + 'Sent an email to Albert with Reference header set to %s.' % id) + + +app = webapp2.WSGIApplication([ + ('/header', SendMailHandler), +], debug=True) diff --git a/appengine/mail/header_test.py b/appengine/mail/header_test.py new file mode 100644 index 00000000000..8c7977b4d3d --- /dev/null +++ b/appengine/mail/header_test.py @@ -0,0 +1,25 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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 header +import webtest + + +def test_send_mail(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + app = webtest.TestApp(header.app) + response = app.post('/header', 'thread_id=42') + assert response.status_int == 200 + assert ('Sent an email to Albert with Reference header set to 42.' + in response.body) diff --git a/appengine/mail/index.html b/appengine/mail/index.html new file mode 100644 index 00000000000..aca2802936d --- /dev/null +++ b/appengine/mail/index.html @@ -0,0 +1,29 @@ + + + + + + Google App Engine Mail Samples + + +

Send email.

+

Send email with a message object.

+

Confirm a user's email address.

+

Send email with attachments.

+

Send email with headers.

+ + \ No newline at end of file diff --git a/appengine/mail/send_mail.py b/appengine/mail/send_mail.py new file mode 100644 index 00000000000..a87052d6a25 --- /dev/null +++ b/appengine/mail/send_mail.py @@ -0,0 +1,48 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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. + +from google.appengine.api import app_identity +from google.appengine.api import mail +import webapp2 + + +def send_approved_mail(sender_address): + # [BEGIN send_mail] + mail.send_mail(sender=sender_address, + to="Albert Johnson ", + subject="Your account has been approved", + body="""Dear Albert: + +Your example.com account has been approved. You can now visit +http://www.example.com/ and sign in using your Google Account to +access new features. + +Please let us know if you have any questions. + +The example.com Team +""") + # [SEND send_mail] + + +class SendMailHandler(webapp2.RequestHandler): + def get(self): + send_approved_mail('%s@appspot.gserviceaccount.com' % + app_identity.get_application_id()) + self.response.content_type = 'text/plain' + self.response.write('Sent an email to Albert.') + + +app = webapp2.WSGIApplication([ + ('/send_mail', SendMailHandler), +], debug=True) diff --git a/appengine/mail/send_mail_test.py b/appengine/mail/send_mail_test.py new file mode 100644 index 00000000000..9417f828797 --- /dev/null +++ b/appengine/mail/send_mail_test.py @@ -0,0 +1,24 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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 send_mail +import webtest + + +def test_send_mail(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + app = webtest.TestApp(send_mail.app) + response = app.get('/send_mail') + assert response.status_int == 200 + assert 'Sent an email to Albert.' in response.body diff --git a/appengine/mail/send_message.py b/appengine/mail/send_message.py new file mode 100644 index 00000000000..45ffc9e715e --- /dev/null +++ b/appengine/mail/send_message.py @@ -0,0 +1,51 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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. + +from google.appengine.api import app_identity +from google.appengine.api import mail +import webapp2 + + +def send_approved_mail(sender_address): + # [BEGIN send_message] + message = mail.EmailMessage( + sender=sender_address, + subject="Your account has been approved") + + message.to = "Albert Johnson " + message.body = """Dear Albert: + +Your example.com account has been approved. You can now visit +http://www.example.com/ and sign in using your Google Account to +access new features. + +Please let us know if you have any questions. + +The example.com Team +""" + message.send() + # [SEND send_message] + + +class SendMessageHandler(webapp2.RequestHandler): + def get(self): + send_approved_mail('%s@appspot.gserviceaccount.com' % + app_identity.get_application_id()) + self.response.content_type = 'text/plain' + self.response.write('Sent an email message to Albert.') + + +app = webapp2.WSGIApplication([ + ('/send_message', SendMessageHandler), +], debug=True) diff --git a/appengine/mail/send_message_test.py b/appengine/mail/send_message_test.py new file mode 100644 index 00000000000..e663cc0bb95 --- /dev/null +++ b/appengine/mail/send_message_test.py @@ -0,0 +1,24 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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 send_message +import webtest + + +def test_send_message(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + app = webtest.TestApp(send_message.app) + response = app.get('/send_message') + assert response.status_int == 200 + assert 'Sent an email message to Albert.' in response.body diff --git a/appengine/mail/user_signup.py b/appengine/mail/user_signup.py new file mode 100644 index 00000000000..56bf95909f5 --- /dev/null +++ b/appengine/mail/user_signup.py @@ -0,0 +1,104 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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 datetime +import random +import socket +import string + +from google.appengine.api import app_identity +from google.appengine.api import mail +from google.appengine.ext import ndb +import webapp2 + + +# [START send-confirm-email] +class UserSignupHandler(webapp2.RequestHandler): + """Serves the email address sign up form.""" + + def post(self): + user_address = self.request.get('email_address') + + if not mail.is_email_valid(user_address): + self.get() # Show the form again. + else: + confirmation_url = create_new_user_confirmation(user_address) + sender_address = ( + 'Example.com Support <%s@appspot.gserviceaccount.com>' % + app_identity.get_application_id()) + subject = 'Confirm your registration' + body = """Thank you for creating an account! +Please confirm your email address by clicking on the link below: + +%s +""" % confirmation_url + mail.send_mail(sender_address, user_address, subject, body) +# [END send-confirm-email] + self.response.content_type = 'text/plain' + self.response.write('An email has been sent to %s.' % user_address) + + def get(self): + self.response.content_type = 'text/html' + self.response.write("""
+ Enter your email address: + +
""") + + +class UserConfirmationRecord(ndb.Model): + """Datastore record with email address and confirmation code.""" + user_address = ndb.StringProperty(indexed=False) + confirmed = ndb.BooleanProperty(indexed=False, default=False) + timestamp = ndb.DateTimeProperty(indexed=False, auto_now_add=True) + + +def create_new_user_confirmation(user_address): + """Create a new user confirmation. + + Args: + user_address: string, an email addres + + Returns: The url to click to confirm the email address.""" + id_chars = string.ascii_letters + string.digits + rand = random.SystemRandom() + random_id = ''.join([rand.choice(id_chars) for i in range(42)]) + record = UserConfirmationRecord(user_address=user_address, + id=random_id) + record.put() + return 'https://%s/user/confirm?code=%s' % ( + socket.getfqdn(socket.gethostname()), random_id) + + +class ConfirmUserSignupHandler(webapp2.RequestHandler): + """Invoked when the user clicks on the confirmation link in the email.""" + + def get(self): + code = self.request.get('code') + if code: + record = ndb.Key(UserConfirmationRecord, code).get() + # 2-hour time limit on confirming. + if record and (datetime.datetime.now() - record.timestamp < + datetime.timedelta(hours=2)): + record.confirmed = True + record.put() + self.response.content_type = 'text/plain' + self.response.write('Confirmed %s.' % record.user_address) + return + self.response.status_int = 404 + + +app = webapp2.WSGIApplication([ + ('/user/signup', UserSignupHandler), + ('/user/confirm', ConfirmUserSignupHandler), +], debug=True) diff --git a/appengine/mail/user_signup_test.py b/appengine/mail/user_signup_test.py new file mode 100644 index 00000000000..41f952819ac --- /dev/null +++ b/appengine/mail/user_signup_test.py @@ -0,0 +1,37 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed 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 user_signup +import webtest + + +def test_user_signup(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + testbed.init_datastore_v3_stub() + app = webtest.TestApp(user_signup.app) + response = app.post('/user/signup', 'email_address=alice@example.com') + assert response.status_int == 200 + assert 'An email has been sent to alice@example.com.' in response.body + + records = user_signup.UserConfirmationRecord.query().fetch(1) + response = app.get('/user/confirm?code=%s' % records[0].key.id()) + assert response.status_int == 200 + assert 'Confirmed alice@example.com.' in response.body + + +def test_bad_code(testbed): + testbed.init_datastore_v3_stub() + app = webtest.TestApp(user_signup.app) + response = app.get('/user/confirm?code=garbage', status=404) + assert response.status_int == 404