@tiledesk/tiledesk-server 2.3.49 → 2.3.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>