Skip to content

Commit bc16c62

Browse files
authored
[TOOLSLIBS-407] Email updates (#173)
* adds opt_in_mode to email * fixes opt_in_mode setter * fixes email uninstall test * adds properties to registration * adds email lookup * fixes tag set test * adds attachment url * wires up EmailAttachment * adds isort config * moves tag imports to prevent circular import * corrects config * adds email attachment class * adds logo image for testing * changes local test file path * adds return to rst * adds attachments to email override * adds email override test * adds more email overrides * formatting * adds full payload test * url safe encoding
1 parent 593d902 commit bc16c62

File tree

10 files changed

+197
-44
lines changed

10 files changed

+197
-44
lines changed

isort.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[ settings ]
2+
extend_skip=__init__.py

tests/data/logo.png

9.15 KB
Loading

tests/devices/test_email.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import base64
12
import json
2-
import mock
33
import unittest
44
import uuid
55

6+
import mock
67
import requests
7-
88
import urbanairship as ua
99
from tests import TEST_KEY, TEST_SECRET
1010

@@ -17,6 +17,7 @@ def setUp(self):
1717
self.locale_language = "en"
1818
self.timezone = "America/Los_Angeles"
1919
self.channel_id = str(uuid.uuid4())
20+
self.opt_in_mode = "double"
2021

2122
def test_email_reg(self):
2223
with mock.patch.object(ua.Airship, "_request") as mock_request:
@@ -89,11 +90,13 @@ def test_email_reg_w_opts(self):
8990
locale_country=self.locale_country,
9091
locale_language=self.locale_language,
9192
timezone=self.timezone,
93+
opt_in_mode=self.opt_in_mode,
9294
)
9395

9496
r = email_obj.register()
9597

9698
self.assertEqual(self.channel_id, email_obj.channel_id)
99+
self.assertEqual(self.opt_in_mode, email_obj.opt_in_mode)
97100
self.assertEqual(201, r.status_code)
98101

99102
def test_email_uninstall(self):
@@ -107,11 +110,29 @@ def test_email_uninstall(self):
107110

108111
email_obj = ua.Email(airship=self.airship, address=self.address)
109112

110-
r = email_obj.register()
113+
r = email_obj.uninstall()
111114

112-
self.assertEqual(self.channel_id, email_obj.channel_id)
113115
self.assertEqual(200, r.status_code)
114116

117+
def test_email_lookup(self):
118+
with mock.patch.object(ua.Airship, "_request") as mock_request:
119+
response = requests.Response()
120+
response._content = json.dumps(
121+
{
122+
"ok": True,
123+
"channel": {"channel_id": self.channel_id, "device_type": "email"},
124+
}
125+
)
126+
response.status_code = 200
127+
mock_request.return_value = response
128+
129+
lookup = ua.Email.lookup(airship=self.airship, address=self.address)
130+
131+
self.assertEqual(200, lookup.status_code)
132+
self.assertEqual(
133+
"email", json.loads(lookup.content)["channel"]["device_type"]
134+
)
135+
115136

116137
class TestEmailTags(unittest.TestCase):
117138
def setUp(self):
@@ -154,7 +175,7 @@ def testTagSet(self):
154175
mock_request.return_value = response
155176

156177
email_tags = ua.EmailTags(airship=self.airship, address=self.address)
157-
email_tags.remove(group=self.test_group, tags=self.test_tags)
178+
email_tags.set(group=self.test_group, tags=self.test_tags)
158179
response = email_tags.send()
159180

160181
self.assertEqual(200, response.status_code)
@@ -172,3 +193,28 @@ def testSetWithAddAndRemove(self):
172193

173194
with self.assertRaises(ValueError):
174195
email_tags.send()
196+
197+
198+
class TestEmailAttachment(unittest.TestCase):
199+
def setUp(self):
200+
self.attachment = ua.EmailAttachment(
201+
airship=ua.Airship(TEST_KEY, TEST_SECRET),
202+
filename="test_file.png",
203+
content_type='image/png; charset="UTF-8"',
204+
filepath="tests/data/logo.png",
205+
)
206+
file = open("tests/data/logo.png", "rb").read()
207+
self.encoded = str(base64.urlsafe_b64encode(file))
208+
209+
def test_encoding(self):
210+
self.assertEqual(self.encoded, self.attachment.req_payload.get("data"))
211+
212+
def test_payload(self):
213+
self.assertDictEqual(
214+
self.attachment.req_payload,
215+
{
216+
"filename": "test_file.png",
217+
"content_type": 'image/png; charset="UTF-8"',
218+
"data": self.encoded,
219+
},
220+
)

tests/devices/test_static_lists.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def test_upload(self):
3131
"alias,marianb".split(","),
3232
"named_user,billg".split(","),
3333
]
34-
self.path = "test_data.csv"
34+
self.path = "tests/data/test_data.csv"
3535

3636
with open(self.path, "wt") as csv_file:
3737
writer = csv.writer(csv_file, delimiter=",")
@@ -188,7 +188,7 @@ def test_next_page(self):
188188
"last_updated": "2015-05-30 23:42:39",
189189
"channel_count": 23,
190190
"status": "processing",
191-
},
191+
}
192192
]
193193
}
194194
).encode("utf-8")

tests/push/test_push.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ def test_email_overrides(self):
380380
sender_name="test_name",
381381
subject="hi",
382382
html_body="<html>so rich!</html>",
383+
attachments=["16d7442e-5ad4-4ab2-b65a-99c63e39a1d6"],
383384
)
384385
)
385386
p.device_types = ua.device_types("email")
@@ -398,6 +399,7 @@ def test_email_overrides(self):
398399
"sender_name": "test_name",
399400
"subject": "hi",
400401
"html_body": "<html>so rich!</html>",
402+
"attachments": [{"id": "16d7442e-5ad4-4ab2-b65a-99c63e39a1d6"}],
401403
}
402404
},
403405
},

urbanairship/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
DeviceInfo,
1616
DeviceTokenList,
1717
Email,
18+
EmailAttachment,
1819
EmailTags,
1920
LocationFinder,
2021
ModifyAttributes,
@@ -187,6 +188,7 @@
187188
Pipeline,
188189
Email,
189190
EmailTags,
191+
EmailAttachment,
190192
CreateAndSendPush,
191193
date_attribute,
192194
text_attribute,

urbanairship/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def __init__(self, location=None):
5959
self.experiments_schedule_url = self.experiments_url + "scheduled/"
6060
self.experiments_validate = self.experiments_url + "validate/"
6161

62+
self.attachment_url = self.base_url + "attachments/"
63+
6264
def get(self, endpoint):
6365
url = getattr(self, endpoint, None)
6466

urbanairship/devices/__init__.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
1-
from .devicelist import (
2-
ChannelList,
3-
ChannelInfo,
4-
DeviceTokenList,
5-
APIDList,
6-
DeviceInfo,
7-
)
8-
91
from .tag import ChannelTags, OpenChannelTags
10-
11-
from .segment import Segment, SegmentList
12-
2+
from .attributes import Attribute, AttributeResponse, ModifyAttributes
133
from .channel_uninstall import ChannelUninstall
14-
15-
from .open_channel import OpenChannel
16-
17-
from .named_users import NamedUser, NamedUserList, NamedUserTags
18-
19-
from .static_lists import (
20-
StaticList,
21-
StaticLists,
22-
)
23-
4+
from .devicelist import APIDList, ChannelInfo, ChannelList, DeviceInfo, DeviceTokenList
5+
from .email import Email, EmailAttachment, EmailTags
246
from .locationfinder import LocationFinder
25-
7+
from .named_users import NamedUser, NamedUserList, NamedUserTags
8+
from .open_channel import OpenChannel
9+
from .segment import Segment, SegmentList
2610
from .sms import Sms
27-
28-
from .email import Email, EmailTags
29-
30-
from .attributes import Attribute, ModifyAttributes, AttributeResponse
11+
from .static_lists import StaticList, StaticLists

urbanairship/devices/email.py

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import json
23
import logging
34
import re
@@ -16,7 +17,7 @@ class Email(object):
1617
1718
Please see the email documentation for important information about
1819
opt-in times and email types.
19-
https://docs.urbanairship.com/api/ua/#operation/api/channels/email
20+
hhttps://docs.airship.com/api/ua/#tag-email
2021
2122
:param address: Required. The email address the object represents.
2223
:param commercial_opted_in: Optional. A string in ISO 8601 format that
@@ -34,6 +35,12 @@ class Email(object):
3435
to locale country setting.
3536
:param locale_language: Optional. The device property tag related
3637
to locale language setting.
38+
:param opt_in_mode: Optional. The opt-in mode for registering the channel. `classic`
39+
is the default when unspecified, `double` creates a `double_opt_in` event.
40+
:param properties: Optional. An object containing event properties. You can use
41+
these properties to filter the double opt-in event and reference them in your
42+
message content by using handlebars. Items in the `properties` object are
43+
limited to a 255-character maximum string length.
3744
:param timezone: Optional. The deice property tag related to
3845
timezone setting.
3946
:param template_fields: For use with CreateAndSend with inline templates.
@@ -50,6 +57,8 @@ def __init__(
5057
transactional_opted_out=None,
5158
locale_country=None,
5259
locale_language=None,
60+
opt_in_mode=None,
61+
properties=None,
5362
timezone=None,
5463
template_fields=None,
5564
):
@@ -61,6 +70,8 @@ def __init__(
6170
self.transactional_opted_out = transactional_opted_out
6271
self.locale_country = locale_country
6372
self.locale_language = locale_language
73+
self.opt_in_mode = opt_in_mode
74+
self.properties = properties
6475
self.timezone = timezone
6576
self.template_fields = template_fields
6677
self._email_type = "email" # only acceptable value at this time
@@ -77,6 +88,17 @@ def template_fields(self, value):
7788

7889
self._template_fields = value
7990

91+
@property
92+
def opt_in_mode(self):
93+
return self._opt_in_mode
94+
95+
@opt_in_mode.setter
96+
def opt_in_mode(self, value):
97+
if value not in ["classic", "double"] and value is not None:
98+
raise ValueError("opt_in_mode must be one of: 'classic' or 'double'")
99+
100+
self._opt_in_mode = value
101+
80102
@property
81103
def address(self):
82104
return self._address
@@ -129,9 +151,7 @@ def transactional_opted_out(self, value):
129151

130152
@property
131153
def create_and_send_audience(self):
132-
audience = {
133-
"ua_address": self.address,
134-
}
154+
audience = {"ua_address": self.address}
135155
if self.commercial_opted_in:
136156
audience["ua_commercial_opted_in"] = self.commercial_opted_in
137157
if self.transactional_opted_in:
@@ -150,12 +170,7 @@ def register(self):
150170
:return: The response object from the API.
151171
"""
152172
url = self.airship.urls.get("email_url")
153-
reg_payload = {
154-
"channel": {
155-
"type": self._email_type,
156-
"address": self.address,
157-
}
158-
}
173+
reg_payload = {"channel": {"type": self._email_type, "address": self.address}}
159174

160175
if self.commercial_opted_in:
161176
reg_payload["channel"]["commercial_opted_in"] = self.commercial_opted_in
@@ -176,6 +191,10 @@ def register(self):
176191
reg_payload["channel"]["locale_country"] = self.locale_country
177192
if self.timezone is not None:
178193
reg_payload["channel"]["timezone"] = self.timezone
194+
if self.opt_in_mode is not None:
195+
reg_payload["channel"]["opt_in_mode"] = self.opt_in_mode
196+
if self.properties is not None:
197+
reg_payload["channel"]["properties"] = self.properties
179198

180199
body = json.dumps(reg_payload).encode("utf-8")
181200

@@ -215,6 +234,17 @@ def uninstall(self):
215234

216235
return response
217236

237+
@classmethod
238+
def lookup(cls, airship, address):
239+
if not VALID_EMAIL.match(address) and address is not None:
240+
raise ValueError("Invalid email address format")
241+
242+
url = airship.urls.get("email_url") + address
243+
244+
response = airship.request(method="GET", url=url, version=3, body=None)
245+
246+
return response
247+
218248

219249
class EmailTags(object):
220250
"""Add, remove or set tags for a list of email addresses
@@ -303,3 +333,54 @@ def send(self):
303333
)
304334

305335
return response
336+
337+
338+
class EmailAttachment(object):
339+
"""
340+
Create an email attachment from a file.
341+
Please see https://docs.airship.com/api/ua/#operation/api/attachments/post
342+
for important information about file size, content type, and send type limitations.
343+
344+
:param filename: Required. The name of the uploaded file (max 255 UTF-8 bytes).
345+
Multiple files with the same name are allowed in separate requests.
346+
:param content_type: Required: The mimetype of the uploaded file including the
347+
charset parameter, if needed.
348+
:param filepath: Required. A path to the file to be uploaded and attached. File must
349+
have permissions set to be opened in 'rb' (binary) mode.
350+
351+
:return: the response object from the API including the 'attachment_ids' uuid to
352+
be used in the email override object.
353+
"""
354+
355+
def __init__(self, airship, filename, content_type, filepath):
356+
self.airship = airship
357+
self.filename = filename
358+
self.content_type = content_type
359+
self.filepath = filepath
360+
361+
def _encode_attachment(self, filepath):
362+
file = open(filepath, "rb").read()
363+
enc = base64.urlsafe_b64encode(file)
364+
365+
return str(enc)
366+
367+
@property
368+
def req_payload(self):
369+
attachment_payload = {
370+
"filename": self.filename,
371+
"content_type": self.content_type,
372+
"data": self._encode_attachment(self.filepath),
373+
}
374+
375+
return attachment_payload
376+
377+
def post(self):
378+
response = self.airship.request(
379+
method="POST",
380+
body=json.dumps(self.req_payload),
381+
url=self.airship.urls.get("attachment_url"),
382+
content_type="application/json",
383+
version=3,
384+
)
385+
386+
return response.json()

0 commit comments

Comments
 (0)