@tiledesk/tiledesk-server 2.3.49 → 2.3.50

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,10 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.48)
7
7
 
8
+ # Untagged
9
+ - Added new email sending endpoint
10
+ - Emails endpoint is now usable by agents
11
+
8
12
  # 2.3.49 -> PROD
9
13
  - @tiledesk/tiledesk-tybot-connector": "^0.1.22
10
14
 
package/app.js CHANGED
@@ -481,7 +481,8 @@ app.use('/:projectid/labels', [fetchLabels],labels);
481
481
 
482
482
  app.use('/:projectid/campaigns',[passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('agent')], campaigns);
483
483
 
484
- app.use('/:projectid/emails',[passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('owner')], email);
484
+ app.use('/:projectid/emails',[passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('agent')], email);
485
+
485
486
 
486
487
 
487
488
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.3.49",
4
+ "version": "2.3.50",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -36,6 +36,9 @@
36
36
  "@tiledesk-ent/tiledesk-server-enterprise": "^1.0.0"
37
37
  },
38
38
  "dependencies": {
39
+ "@tiledesk/tiledesk-apps": "^1.0.6",
40
+ "@tiledesk/tiledesk-whatsapp-connector": "^0.1.19",
41
+ "@tiledesk/tiledesk-kaleyra-proxy": "^0.1.5",
39
42
  "@tiledesk/tiledesk-chat21-app": "^1.1.7",
40
43
  "@tiledesk/tiledesk-chatbot-util": "^0.8.33",
41
44
  "@tiledesk/tiledesk-json-rules-engine": "^4.0.3",
@@ -0,0 +1,8 @@
1
+ const listener = require("./listener");
2
+
3
+ const apps = require("@tiledesk/tiledesk-apps");
4
+ const appsRoute = apps.router;
5
+
6
+
7
+ module.exports = { listener: listener, appsRoute: appsRoute }
8
+
@@ -0,0 +1,27 @@
1
+ const apps = require("@tiledesk/tiledesk-apps");
2
+ var winston = require('../../config/winston');
3
+
4
+ class Listener {
5
+
6
+ listen(config) {
7
+
8
+ winston.info("Apps Listener listen");
9
+
10
+ if (config.databaseUri) {
11
+ winston.debug("apps config databaseUri: " + config.databaseUri);
12
+ }
13
+
14
+ apps.startApp({
15
+ ACCESS_TOKEN_SECRET: process.env.APPS_ACCESS_TOKEN_SECRET || 'nodeauthsecret',
16
+ MONGODB_URI: process.env.APPS_MONGODB_URI || config.databaseUri,
17
+ }, () => {
18
+ winston.info("Tiledesk Apps proxy server succesfully started.")
19
+ })
20
+
21
+ }
22
+
23
+ }
24
+
25
+ var listener = new Listener();
26
+
27
+ module.exports = listener;
@@ -0,0 +1,6 @@
1
+ const listener = require('./listener');
2
+
3
+ const kaleyra = require('@tiledesk/tiledesk-kaleyra-proxy');
4
+ const kaleyraRoute = kaleyra.router;
5
+
6
+ module.exports = { listener: listener, kaleyraRoute: kaleyraRoute }
@@ -0,0 +1,32 @@
1
+ const kaleyra = require("@tiledesk/tiledesk-kaleyra-proxy");
2
+ var winston = require('../../config/winston');
3
+ var configGlobal = require('../../config/global');
4
+
5
+ const apiUrl = process.env.API_URL || configGlobal.apiUrl;
6
+ winston.info("Kaleyra apiUrl: " + apiUrl);
7
+
8
+ class Listener {
9
+
10
+ listen(config) {
11
+ winston.info("Kaleyra Listener listen");
12
+ if (config.databaseUri) {
13
+ winston.debug("kaleyra config databaseUri: " + config.databaseUri);
14
+ }
15
+
16
+ kaleyra.startApp({
17
+ MONGODB_URL: config.databaseUri,
18
+ API_URL: apiUrl,
19
+ BASE_URL: apiUrl + "/modules/kaleyra",
20
+ APPS_API_URL: apiUrl + "/modules/apps",
21
+ KALEYRA_API_URL: process.env.KALEYRA_API_URL,
22
+ API_KEY: process.env.API_KEY,
23
+ log: process.env.KALEYRA_LOG
24
+ }, () => {
25
+ winston.info("Tiledesk Kaleyra proxy server succesfully started.");
26
+ })
27
+ }
28
+ }
29
+
30
+ var listener = new Listener();
31
+
32
+ module.exports = listener;
@@ -21,6 +21,15 @@ class PubModulesManager {
21
21
  this.rasa = undefined;
22
22
  this.rasaRoute = undefined;
23
23
 
24
+ this.apps = undefined;
25
+ this.appsRoute = undefined;
26
+
27
+ this.whatsapp = undefined;
28
+ this.whatsappRoute = undefined;
29
+
30
+ this.kaleyra = undefined;
31
+ this.kaleyraRoute = undefined;
32
+
24
33
  this.activityArchiver = undefined;
25
34
  this.activityRoute = undefined;
26
35
 
@@ -53,6 +62,18 @@ class PubModulesManager {
53
62
  app.use('/modules/rasa', this.rasaRoute);
54
63
  winston.info("ModulesManager rasaRoute controller loaded");
55
64
  }
65
+ if (this.appsRoute) {
66
+ app.use('/modules/apps', this.appsRoute);
67
+ winston.info("ModulesManager appsRoute controller loaded");
68
+ }
69
+ if (this.whatsappRoute) {
70
+ app.use('/modules/whatsapp', this.whatsappRoute);
71
+ winston.info("ModulesManager whatsappRoute controller loaded");
72
+ }
73
+ if (this.kaleyraRoute) {
74
+ app.use('/modules/kaleyra', this.kaleyraRoute);
75
+ winston.info("ModulesManager kaleyraRoute controller loaded");
76
+ }
56
77
  if (this.tilebotRoute) {
57
78
  app.use('/modules/tilebot', this.tilebotRoute);
58
79
  winston.info("ModulesManager tilebot controller loaded");
@@ -217,7 +238,53 @@ class PubModulesManager {
217
238
  }
218
239
  }
219
240
 
241
+ try {
242
+ this.apps = require('./apps');
243
+ winston.debug("this.apps: " + this.apps);
244
+ this.apps.listener.listen(config);
245
+
246
+ this.appsRoute = this.apps.appsRoute;
247
+
248
+ winston.info("PubModulesManager initialized apps.");
249
+ } catch(err) {
250
+ if (err.code == 'MODULE_NOT_FOUND') {
251
+ winston.info("PubModulesManager init apps module not found");
252
+ }else {
253
+ winston.info("PubModulesManager error initializing init apps module", err);
254
+ }
255
+ }
256
+
257
+ try {
258
+ this.whatsapp = require('./whatsapp');
259
+ winston.debug("this.whatsapp: " + this.whatsapp);
260
+ this.whatsapp.listener.listen(config);
220
261
 
262
+ this.whatsappRoute = this.whatsapp.whatsappRoute;
263
+
264
+ winston.info("PubModulesManager initialized apps.");
265
+ } catch(err) {
266
+ if (err.code == 'MODULE_NOT_FOUND') {
267
+ winston.info("PubModulesManager init apps module not found");
268
+ }else {
269
+ winston.info("PubModulesManager error initializing init apps module", err);
270
+ }
271
+ }
272
+
273
+ try {
274
+ this.kaleyra = require('./kaleyra');
275
+ winston.debug("this.kaleyra: " + this.kaleyra);
276
+ this.kaleyra.listener.listen(config);
277
+
278
+ this.kaleyraRoute = this.kaleyra.kaleyraRoute;
279
+
280
+ winston.info("PubModulesManager initialized apps.");
281
+ } catch(err) {
282
+ if (err.code == 'MODULE_NOT_FOUND') {
283
+ winston.info("PubModulesManager init apps module not found");
284
+ }else {
285
+ winston.info("PubModulesManager error initializing init apps module", err);
286
+ }
287
+ }
221
288
 
222
289
  try {
223
290
  this.activityArchiver = require('./activities').activityArchiver;
@@ -0,0 +1,7 @@
1
+ const listener = require("./listener");
2
+
3
+ const whatsapp = require("@tiledesk/tiledesk-whatsapp-connector");
4
+ const whatsappRoute = whatsapp.router;
5
+
6
+
7
+ module.exports = { listener: listener, whatsappRoute: whatsappRoute }
@@ -0,0 +1,32 @@
1
+ const whatsapp = require("@tiledesk/tiledesk-whatsapp-connector");
2
+ var winston = require('../../config/winston');
3
+ var configGlobal = require('../../config/global');
4
+
5
+ const apiUrl = process.env.API_URL || configGlobal.apiUrl;
6
+ winston.info('Whatsapp apiUrl: ' + apiUrl);
7
+
8
+ class Listener {
9
+
10
+ listen(config) {
11
+ winston.info("WhatsApp Listener listen");
12
+ if (config.databaseUri) {
13
+ winston.debug("whatsapp config databaseUri: " + config.databaseUri);
14
+ }
15
+
16
+ whatsapp.startApp({
17
+ MONGODB_URL: config.databaseUri,
18
+ API_URL: apiUrl,
19
+ GRAPH_URL: process.env.META_GRAPH_URL || config.graphUrl,
20
+ BASE_URL: apiUrl + "/modules/whatsapp",
21
+ APPS_API_URL: apiUrl + "/modules/apps",
22
+ log: process.env.WHATSAPP_LOG
23
+ }, () => {
24
+ winston.info("Tiledesk WhatsApp Connector proxy server succesfully started.");
25
+ })
26
+
27
+ }
28
+ }
29
+
30
+ var listener = new Listener();
31
+
32
+ module.exports = listener;
package/routes/email.js CHANGED
@@ -17,10 +17,10 @@ router.get('/templates/:templateid',
17
17
  router.post('/test/send',
18
18
  async (req, res) => {
19
19
  let to = req.body.to;
20
- winston.info("to",to);
20
+ winston.debug("to",to);
21
21
 
22
22
  let configEmail = req.body.config;
23
- winston.info("configEmail", configEmail);
23
+ winston.debug("configEmail", configEmail);
24
24
 
25
25
  emailService.sendTest(to, configEmail, function(err,obj) {
26
26
  // winston.info("sendTest rest", err, obj);
@@ -29,4 +29,28 @@ router.post('/test/send',
29
29
 
30
30
  });
31
31
 
32
+
33
+ router.post('/send',
34
+ async (req, res) => {
35
+ let to = req.body.to;
36
+ winston.info("to: " + to);
37
+
38
+ let text = req.body.text;
39
+ winston.info("text: " + text);
40
+
41
+ let request_id = req.body.request_id;
42
+ winston.info("request_id: " + request_id);
43
+
44
+ let subject = req.body.subject;
45
+ winston.info("subject: " + subject);
46
+
47
+ winston.info("req.project", req.project);
48
+
49
+ //sendEmailDirect(to, text, project, request_id, subject, tokenQueryString, sourcePage)
50
+ emailService.sendEmailDirect(to, text, req.project, request_id, subject, undefined, undefined);
51
+
52
+ res.json({"queued": true});
53
+
54
+ });
55
+
32
56
  module.exports = router;
@@ -1030,7 +1030,7 @@ class EmailService {
1030
1030
 
1031
1031
  }
1032
1032
 
1033
-
1033
+
1034
1034
 
1035
1035
  async sendEmailChannelNotification(to, message, project, tokenQueryString, sourcePage) {
1036
1036
 
@@ -1360,10 +1360,6 @@ class EmailService {
1360
1360
  }
1361
1361
 
1362
1362
 
1363
-
1364
-
1365
-
1366
-
1367
1363
  /*
1368
1364
  sendEmailChannelTakingNotification(to, request, project, tokenQueryString) {
1369
1365
 
@@ -1411,6 +1407,102 @@ class EmailService {
1411
1407
  }
1412
1408
  */
1413
1409
 
1410
+
1411
+
1412
+
1413
+
1414
+
1415
+
1416
+ async sendEmailDirect(to, text, project, request_id, subject, tokenQueryString, sourcePage) {
1417
+
1418
+ var that = this;
1419
+
1420
+
1421
+ if (project.toJSON) {
1422
+ project = project.toJSON();
1423
+ }
1424
+
1425
+ var html = await this.readTemplate('emailDirect.html', project.settings);
1426
+
1427
+ var envTemplate = process.env.EMAIL_DIRECT_HTML_TEMPLATE;
1428
+ winston.debug("envTemplate: " + envTemplate);
1429
+
1430
+ if (envTemplate) {
1431
+ html = envTemplate;
1432
+ }
1433
+
1434
+ winston.debug("html: " + html);
1435
+
1436
+ var template = handlebars.compile(html);
1437
+
1438
+ var baseScope = JSON.parse(JSON.stringify(that));
1439
+ delete baseScope.pass;
1440
+
1441
+
1442
+ let msgText = text;
1443
+ msgText = encode(msgText);
1444
+ if (this.markdown) {
1445
+ msgText = marked(msgText);
1446
+ }
1447
+
1448
+ winston.debug("msgText: " + msgText);
1449
+ winston.debug("baseScope: " + JSON.stringify(baseScope));
1450
+
1451
+
1452
+ var replacements = {
1453
+ project: project,
1454
+ seamlessPage: sourcePage,
1455
+ msgText: msgText,
1456
+ tokenQueryString: tokenQueryString,
1457
+ baseScope: baseScope
1458
+ };
1459
+
1460
+ var html = template(replacements);
1461
+ winston.debug("html: " + html);
1462
+
1463
+
1464
+ let replyTo;
1465
+ if (this.replyEnabled) {
1466
+ replyTo = request_id + this.inboundDomainDomainWithAt;
1467
+ }
1468
+
1469
+ let from;
1470
+ let configEmail;
1471
+ if (project && project.settings && project.settings.email) {
1472
+ if (project.settings.email.config) {
1473
+ configEmail = project.settings.email.config;
1474
+ winston.verbose("custom email configEmail setting found: ", configEmail);
1475
+ }
1476
+ if (project.settings.email.from) {
1477
+ from = project.settings.email.from;
1478
+ winston.verbose("custom from email setting found: "+ from);
1479
+ }
1480
+ }
1481
+
1482
+
1483
+ // if (message.request && message.request.lead && message.request.lead.email) {
1484
+ // winston.info("message.request.lead.email: " + message.request.lead.email);
1485
+ // replyTo = replyTo + ", "+ message.request.lead.email;
1486
+ // }
1487
+
1488
+ // if (!subject) {
1489
+ // subject = "Tiledesk"
1490
+ // }
1491
+
1492
+ that.send({
1493
+ from:from,
1494
+ to:to,
1495
+ replyTo: replyTo,
1496
+ subject:subject,
1497
+ text:html,
1498
+ html:html,
1499
+ config:configEmail,
1500
+ });
1501
+
1502
+ }
1503
+
1504
+
1505
+
1414
1506
  // ok
1415
1507
  async sendPasswordResetRequestEmail(to, resetPswRequestId, userFirstname, userLastname) {
1416
1508
 
@@ -1693,6 +1785,5 @@ async sendRequestTranscript(to, messages, request, project) {
1693
1785
 
1694
1786
  var emailService = new EmailService();
1695
1787
 
1696
- // emailService.sendTest("abc@abc.it");
1697
1788
 
1698
1789
  module.exports = emailService;
@@ -94,7 +94,7 @@
94
94
  {{currentUserFirstname}} {{currentUserLastname}} has invited you to the TileDesk project <strong> {{projectName}}</strong>
95
95
  </h2>
96
96
 
97
- <br> <br<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Hi {{invitedUserFirstname}} {{invitedUserLastname}},</strong>
97
+ <br> <br><strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Hi {{invitedUserFirstname}} {{invitedUserLastname}},</strong>
98
98
 
99
99
  <br> <br>
100
100
  I invited you to take on the role of {{invitedUserRole}} of the TileDesk <strong> {{projectName}}</strong> project
@@ -95,7 +95,7 @@
95
95
  {{currentUserFirstname}} {{currentUserLastname}} has invited you to the Tiledesk project <strong> {{projectName}}</strong>
96
96
  </h2>
97
97
 
98
- <br> <br<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{{to}},</strong>
98
+ <br> <br><strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{{to}},</strong>
99
99
 
100
100
  <br> <br>
101
101
  I invited you to take on the role of {{invitedUserRole}} of the Tiledesk <strong> {{projectName}}</strong> project
@@ -0,0 +1,121 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html lang="en" xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
3
+
4
+ <head>
5
+ <meta name="viewport" content="width=device-width" />
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7
+ <title>New Ticket from TileDesk</title>
8
+
9
+ <style type="text/css">
10
+ img {
11
+ max-width: 100%;
12
+ margin-left:16px;
13
+ margin-bottom:16px;
14
+ text-align:center !important;
15
+ }
16
+ body {
17
+ -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
18
+ }
19
+ body {
20
+ background-color: #f6f6f6;
21
+ }
22
+
23
+ @media only screen and (max-width: 640px) {
24
+ body {
25
+ padding: 0 !important;
26
+ }
27
+ h1 {
28
+ font-weight: 800 !important; margin: 20px 0 5px !important;
29
+ text-align:center !important;
30
+ }
31
+ h2 {
32
+ font-weight: 800 !important; margin: 20px 0 5px !important;
33
+ }
34
+ h3 {
35
+ font-weight: 800 !important; margin: 20px 0 5px !important;
36
+ }
37
+ h4 {
38
+ font-weight: 800 !important; margin: 20px 0 5px !important;
39
+ }
40
+ h1 {
41
+ font-size: 22px !important;
42
+ }
43
+ h2 {
44
+ font-size: 18px !important;
45
+ }
46
+ h3 {
47
+ font-size: 16px !important;
48
+ }
49
+ .container {
50
+ padding: 0 !important; width: 100% !important;
51
+ }
52
+ .content {
53
+ padding: 0 !important;
54
+ }
55
+ .content-wrap {
56
+ padding: 10px !important;
57
+ }
58
+ .invoice {
59
+ width: 100% !important;
60
+ }
61
+ }
62
+ </style>
63
+ </head>
64
+
65
+ <body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
66
+
67
+ {{#if baseScope.replyEnabled}}
68
+ <div>\# Please type your reply above this line \#</div>
69
+ {{/if}}
70
+
71
+ <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff">
72
+
73
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
74
+ <td class="alert alert-warning" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; margin: 0;" align="center" valign="top">
75
+
76
+ <div style="text-align:center">
77
+ <a href="http://www.tiledesk.com" style="color:#2daae1;font-weight:bold;text-decoration:none;word-break:break-word" target="_blank">
78
+ <!-- <img src="https://tiledesk.com/wp-content/uploads/2022/07/tiledesk_v2.png" style="width:20%;outline:none;text-decoration:none;border:none;min-height:36px" class="CToWUd"> -->
79
+ <img src="https://tiledesk.com/wp-content/uploads/2022/09/tiledeesk_log_email.png" style="max-width:200px;outline:none;text-decoration:none;border:none;height:auto;margin-left:0px;" class="CToWUd">
80
+ </a>
81
+ </div>
82
+ </td>
83
+ </tr>
84
+
85
+
86
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
87
+ <td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
88
+ <table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
89
+
90
+
91
+
92
+
93
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
94
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
95
+ <div style="white-space: pre-wrap;">{{{msgText}}}</div>
96
+ </td>
97
+ </tr>
98
+
99
+
100
+
101
+
102
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
103
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
104
+ </td>
105
+ </tr>
106
+ </table>
107
+ </td>
108
+ </tr>
109
+ </table>
110
+ <div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
111
+ <table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
112
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
113
+ <td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">
114
+ <span><a href="http://www.tiledesk.com" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;" > Tiledesk.com </a></span>
115
+ <!-- <br><span><a href="%unsubscribe_url%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">Unsubscribe</a></span> -->
116
+ </td>
117
+ </tr>
118
+ </table>
119
+
120
+ </body>
121
+ </html>