OLD | NEW |
1 # Copyright (c) 2010 Mitch Garnaat http://garnaat.org/ | 1 # Copyright (c) 2010 Mitch Garnaat http://garnaat.org/ |
2 # Copyright (c) 2011 Harry Marr http://hmarr.com/ | 2 # Copyright (c) 2011 Harry Marr http://hmarr.com/ |
3 # | 3 # |
4 # Permission is hereby granted, free of charge, to any person obtaining a | 4 # Permission is hereby granted, free of charge, to any person obtaining a |
5 # copy of this software and associated documentation files (the | 5 # copy of this software and associated documentation files (the |
6 # "Software"), to deal in the Software without restriction, including | 6 # "Software"), to deal in the Software without restriction, including |
7 # without limitation the rights to use, copy, modify, merge, publish, dis- | 7 # without limitation the rights to use, copy, modify, merge, publish, dis- |
8 # tribute, sublicense, and/or sell copies of the Software, and to permit | 8 # tribute, sublicense, and/or sell copies of the Software, and to permit |
9 # persons to whom the Software is furnished to do so, subject to the fol- | 9 # persons to whom the Software is furnished to do so, subject to the fol- |
10 # lowing conditions: | 10 # lowing conditions: |
11 # | 11 # |
12 # The above copyright notice and this permission notice shall be included | 12 # The above copyright notice and this permission notice shall be included |
13 # in all copies or substantial portions of the Software. | 13 # in all copies or substantial portions of the Software. |
14 # | 14 # |
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- | 16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | 17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | 18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
21 # IN THE SOFTWARE. | 21 # IN THE SOFTWARE. |
22 | 22 |
23 from boto.connection import AWSAuthConnection | 23 from boto.connection import AWSAuthConnection |
24 from boto.exception import BotoServerError | 24 from boto.exception import BotoServerError |
| 25 from boto.regioninfo import RegionInfo |
25 import boto | 26 import boto |
26 import boto.jsonresponse | 27 import boto.jsonresponse |
27 | 28 |
28 import urllib | 29 import urllib |
29 import base64 | 30 import base64 |
30 | 31 |
31 | 32 |
32 class SESConnection(AWSAuthConnection): | 33 class SESConnection(AWSAuthConnection): |
33 | 34 |
34 ResponseError = BotoServerError | 35 ResponseError = BotoServerError |
35 DefaultHost = 'email.us-east-1.amazonaws.com' | 36 DefaultRegionName = 'us-east-1' |
| 37 DefaultRegionEndpoint = 'email.us-east-1.amazonaws.com' |
36 APIVersion = '2010-12-01' | 38 APIVersion = '2010-12-01' |
37 | 39 |
38 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, | 40 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, |
39 port=None, proxy=None, proxy_port=None, | 41 is_secure=True, port=None, proxy=None, proxy_port=None, |
40 host=DefaultHost, debug=0): | 42 proxy_user=None, proxy_pass=None, debug=0, |
41 AWSAuthConnection.__init__(self, host, aws_access_key_id, | 43 https_connection_factory=None, region=None, path='/'): |
42 aws_secret_access_key, True, port, proxy, | 44 if not region: |
43 proxy_port, debug=debug) | 45 region = RegionInfo(self, self.DefaultRegionName, |
| 46 self.DefaultRegionEndpoint) |
| 47 self.region = region |
| 48 AWSAuthConnection.__init__(self, self.region.endpoint, |
| 49 aws_access_key_id, aws_secret_access_key, |
| 50 is_secure, port, proxy, proxy_port, |
| 51 proxy_user, proxy_pass, debug, |
| 52 https_connection_factory, path) |
44 | 53 |
45 def _required_auth_capability(self): | 54 def _required_auth_capability(self): |
46 return ['ses'] | 55 return ['ses'] |
47 | 56 |
48 def _build_list_params(self, params, items, label): | 57 def _build_list_params(self, params, items, label): |
49 """Add an AWS API-compatible parameter list to a dictionary. | 58 """Add an AWS API-compatible parameter list to a dictionary. |
50 | 59 |
51 :type params: dict | 60 :type params: dict |
52 :param params: The parameter dictionary | 61 :param params: The parameter dictionary |
53 | 62 |
54 :type items: list | 63 :type items: list |
55 :param items: Items to be included in the list | 64 :param items: Items to be included in the list |
56 | 65 |
57 :type label: string | 66 :type label: string |
58 :param label: The parameter list's name | 67 :param label: The parameter list's name |
59 """ | 68 """ |
60 if isinstance(items, str): | 69 if isinstance(items, basestring): |
61 items = [items] | 70 items = [items] |
62 for i in range(1, len(items) + 1): | 71 for i in range(1, len(items) + 1): |
63 params['%s.%d' % (label, i)] = items[i - 1] | 72 params['%s.%d' % (label, i)] = items[i - 1] |
64 | 73 |
65 | 74 |
66 def _make_request(self, action, params=None): | 75 def _make_request(self, action, params=None): |
67 """Make a call to the SES API. | 76 """Make a call to the SES API. |
68 | 77 |
69 :type action: string | 78 :type action: string |
70 :param action: The API method to use (e.g. SendRawEmail) | 79 :param action: The API method to use (e.g. SendRawEmail) |
71 | 80 |
72 :type params: dict | 81 :type params: dict |
73 :param params: Parameters that will be sent as POST data with the API | 82 :param params: Parameters that will be sent as POST data with the API |
74 call. | 83 call. |
75 """ | 84 """ |
76 headers = {'Content-Type': 'application/x-www-form-urlencoded'} | 85 ct = 'application/x-www-form-urlencoded; charset=UTF-8' |
| 86 headers = {'Content-Type': ct} |
77 params = params or {} | 87 params = params or {} |
78 params['Action'] = action | 88 params['Action'] = action |
| 89 |
| 90 for k, v in params.items(): |
| 91 if isinstance(v, unicode): # UTF-8 encode only if it's Unicode |
| 92 params[k] = v.encode('utf-8') |
| 93 |
79 response = super(SESConnection, self).make_request( | 94 response = super(SESConnection, self).make_request( |
80 'POST', | 95 'POST', |
81 '/', | 96 '/', |
82 headers=headers, | 97 headers=headers, |
83 data=urllib.urlencode(params) | 98 data=urllib.urlencode(params) |
84 ) | 99 ) |
85 body = response.read() | 100 body = response.read() |
86 if response.status == 200: | 101 if response.status == 200: |
87 list_markers = ('VerifiedEmailAddresses', 'SendDataPoints') | 102 list_markers = ('VerifiedEmailAddresses', 'SendDataPoints') |
88 e = boto.jsonresponse.Element(list_marker=list_markers) | 103 e = boto.jsonresponse.Element(list_marker=list_markers) |
89 h = boto.jsonresponse.XmlHandler(e, None) | 104 h = boto.jsonresponse.XmlHandler(e, None) |
90 h.parse(body) | 105 h.parse(body) |
91 return e | 106 return e |
92 else: | 107 else: |
93 boto.log.error('%s %s' % (response.status, response.reason)) | 108 boto.log.error('%s %s' % (response.status, response.reason)) |
94 boto.log.error('%s' % body) | 109 boto.log.error('%s' % body) |
95 raise self.ResponseError(response.status, response.reason, body) | 110 raise self.ResponseError(response.status, response.reason, body) |
96 | 111 |
97 | 112 |
98 def send_email(self, source, subject, body, to_addresses, cc_addresses=None, | 113 def send_email(self, source, subject, body, to_addresses, cc_addresses=None, |
99 bcc_addresses=None, format='text'): | 114 bcc_addresses=None, format='text', reply_addresses=None, |
| 115 return_path=None, text_body=None, html_body=None): |
100 """Composes an email message based on input data, and then immediately | 116 """Composes an email message based on input data, and then immediately |
101 queues the message for sending. | 117 queues the message for sending. |
102 | 118 |
103 :type source: string | 119 :type source: string |
104 :param source: The sender's email address. | 120 :param source: The sender's email address. |
105 | 121 |
106 :type subject: string | 122 :type subject: string |
107 :param subject: The subject of the message: A short summary of the | 123 :param subject: The subject of the message: A short summary of the |
108 content, which will appear in the recipient's inbox. | 124 content, which will appear in the recipient's inbox. |
109 | 125 |
110 :type body: string | 126 :type body: string |
111 :param body: The message body. | 127 :param body: The message body. |
112 | 128 |
113 :type to_addresses: list of strings or string | 129 :type to_addresses: list of strings or string |
114 :param to_addresses: The To: field(s) of the message. | 130 :param to_addresses: The To: field(s) of the message. |
115 | 131 |
116 :type cc_addresses: list of strings or string | 132 :type cc_addresses: list of strings or string |
117 :param cc_addresses: The CC: field(s) of the message. | 133 :param cc_addresses: The CC: field(s) of the message. |
118 | 134 |
119 :type bcc_addresses: list of strings or string | 135 :type bcc_addresses: list of strings or string |
120 :param bcc_addresses: The BCC: field(s) of the message. | 136 :param bcc_addresses: The BCC: field(s) of the message. |
121 | 137 |
122 :type format: string | 138 :type format: string |
123 :param format: The format of the message's body, must be either "text" | 139 :param format: The format of the message's body, must be either "text" |
124 or "html". | 140 or "html". |
125 | 141 |
| 142 :type reply_addresses: list of strings or string |
| 143 :param reply_addresses: The reply-to email address(es) for the |
| 144 message. If the recipient replies to the |
| 145 message, each reply-to address will |
| 146 receive the reply. |
| 147 |
| 148 :type return_path: string |
| 149 :param return_path: The email address to which bounce notifications are |
| 150 to be forwarded. If the message cannot be delivered |
| 151 to the recipient, then an error message will be |
| 152 returned from the recipient's ISP; this message will |
| 153 then be forwarded to the email address specified by |
| 154 the ReturnPath parameter. |
| 155 |
| 156 :type text_body: string |
| 157 :param text_body: The text body to send with this email. |
| 158 |
| 159 :type html_body: string |
| 160 :param html_body: The html body to send with this email. |
| 161 |
126 """ | 162 """ |
| 163 format = format.lower().strip() |
| 164 if body is not None: |
| 165 if format == "text": |
| 166 if text_body is not None: |
| 167 raise Warning("You've passed in both a body and a text_body;
please choose one or the other.") |
| 168 text_body = body |
| 169 else: |
| 170 if html_body is not None: |
| 171 raise Warning("You've passed in both a body and an html_body
; please choose one or the other.") |
| 172 html_body = body |
| 173 |
127 params = { | 174 params = { |
128 'Source': source, | 175 'Source': source, |
129 'Message.Subject.Data': subject, | 176 'Message.Subject.Data': subject, |
130 } | 177 } |
131 | 178 |
132 format = format.lower().strip() | 179 if return_path: |
133 if format == 'html': | 180 params['ReturnPath'] = return_path |
134 params['Message.Body.Html.Data'] = body | 181 |
135 elif format == 'text': | 182 if html_body is not None: |
136 params['Message.Body.Text.Data'] = body | 183 params['Message.Body.Html.Data'] = html_body |
137 else: | 184 if text_body is not None: |
| 185 params['Message.Body.Text.Data'] = text_body |
| 186 |
| 187 if(format not in ("text","html")): |
138 raise ValueError("'format' argument must be 'text' or 'html'") | 188 raise ValueError("'format' argument must be 'text' or 'html'") |
139 | 189 |
| 190 if(not (html_body or text_body)): |
| 191 raise ValueError("No text or html body found for mail") |
| 192 |
140 self._build_list_params(params, to_addresses, | 193 self._build_list_params(params, to_addresses, |
141 'Destination.ToAddresses.member') | 194 'Destination.ToAddresses.member') |
142 if cc_addresses: | 195 if cc_addresses: |
143 self._build_list_params(params, cc_addresses, | 196 self._build_list_params(params, cc_addresses, |
144 'Destination.CcAddresses.member') | 197 'Destination.CcAddresses.member') |
145 | 198 |
146 if bcc_addresses: | 199 if bcc_addresses: |
147 self._build_list_params(params, bcc_addresses, | 200 self._build_list_params(params, bcc_addresses, |
148 'Destination.BccAddresses.member') | 201 'Destination.BccAddresses.member') |
149 | 202 |
| 203 if reply_addresses: |
| 204 self._build_list_params(params, reply_addresses, |
| 205 'ReplyToAddresses.member') |
| 206 |
150 return self._make_request('SendEmail', params) | 207 return self._make_request('SendEmail', params) |
151 | 208 |
152 def send_raw_email(self, source, raw_message, destinations=None): | 209 def send_raw_email(self, raw_message, source=None, destinations=None): |
153 """Sends an email message, with header and content specified by the | 210 """Sends an email message, with header and content specified by the |
154 client. The SendRawEmail action is useful for sending multipart MIME | 211 client. The SendRawEmail action is useful for sending multipart MIME |
155 emails, with attachments or inline content. The raw text of the message | 212 emails, with attachments or inline content. The raw text of the message |
156 must comply with Internet email standards; otherwise, the message | 213 must comply with Internet email standards; otherwise, the message |
157 cannot be sent. | 214 cannot be sent. |
158 | 215 |
159 :type source: string | 216 :type source: string |
160 :param source: The sender's email address. | 217 :param source: The sender's email address. Amazon's docs say: |
| 218 |
| 219 If you specify the Source parameter, then bounce notifications and |
| 220 complaints will be sent to this email address. This takes precedence |
| 221 over any Return-Path header that you might include in the raw text of |
| 222 the message. |
161 | 223 |
162 :type raw_message: string | 224 :type raw_message: string |
163 :param raw_message: The raw text of the message. The client is | 225 :param raw_message: The raw text of the message. The client is |
164 responsible for ensuring the following: | 226 responsible for ensuring the following: |
165 | 227 |
166 - Message must contain a header and a body, separated by a blank line. | 228 - Message must contain a header and a body, separated by a blank line. |
167 - All required header fields must be present. | 229 - All required header fields must be present. |
168 - Each part of a multipart MIME message must be formatted properly. | 230 - Each part of a multipart MIME message must be formatted properly. |
169 - MIME content types must be among those supported by Amazon SES. | 231 - MIME content types must be among those supported by Amazon SES. |
170 Refer to the Amazon SES Developer Guide for more details. | 232 Refer to the Amazon SES Developer Guide for more details. |
171 - Content must be base64-encoded, if MIME requires it. | 233 - Content must be base64-encoded, if MIME requires it. |
172 | 234 |
173 :type destinations: list of strings or string | 235 :type destinations: list of strings or string |
174 :param destinations: A list of destinations for the message. | 236 :param destinations: A list of destinations for the message. |
175 | 237 |
176 """ | 238 """ |
177 params = { | 239 params = { |
178 'Source': source, | |
179 'RawMessage.Data': base64.b64encode(raw_message), | 240 'RawMessage.Data': base64.b64encode(raw_message), |
180 } | 241 } |
| 242 |
| 243 if source: |
| 244 params['Source'] = source |
181 | 245 |
182 self._build_list_params(params, destinations, | 246 if destinations: |
183 'Destinations.member') | 247 self._build_list_params(params, destinations, |
| 248 'Destinations.member') |
184 | 249 |
185 return self._make_request('SendRawEmail', params) | 250 return self._make_request('SendRawEmail', params) |
186 | 251 |
187 def list_verified_email_addresses(self): | 252 def list_verified_email_addresses(self): |
188 """Fetch a list of the email addresses that have been verified. | 253 """Fetch a list of the email addresses that have been verified. |
189 | 254 |
190 :rtype: dict | 255 :rtype: dict |
191 :returns: A ListVerifiedEmailAddressesResponse structure. Note that | 256 :returns: A ListVerifiedEmailAddressesResponse structure. Note that |
192 keys must be unicode strings. | 257 keys must be unicode strings. |
193 """ | 258 """ |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
238 :type email_adddress: string | 303 :type email_adddress: string |
239 :param email_address: The email address to be verified. | 304 :param email_address: The email address to be verified. |
240 | 305 |
241 :rtype: dict | 306 :rtype: dict |
242 :returns: A VerifyEmailAddressResponse structure. Note that keys must | 307 :returns: A VerifyEmailAddressResponse structure. Note that keys must |
243 be unicode strings. | 308 be unicode strings. |
244 """ | 309 """ |
245 return self._make_request('VerifyEmailAddress', { | 310 return self._make_request('VerifyEmailAddress', { |
246 'EmailAddress': email_address, | 311 'EmailAddress': email_address, |
247 }) | 312 }) |
248 | |
OLD | NEW |