Merge pull request #4814 from atif-shaikh/CRM-14199Improvement
[civicrm-core.git] / js / angular-crmMailing / services.js
1 (function (angular, $, _) {
2 var partialUrl = function (relPath) {
3 return CRM.resourceUrls['civicrm'] + '/partials/crmMailing/' + relPath;
4 };
5
6 // The representation of from/reply-to addresses is inconsistent in the mailing data-model,
7 // so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
8 // the available "From:" addrs. Records are like the underlying OptionValues -- but add "email"
9 // and "author".
10 angular.module('crmMailing').factory('crmFromAddresses', function ($q, crmApi) {
11 var emailRegex = /^"(.*)" \<([^@\>]*@[^@\>]*)\>$/;
12 var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
13 var match = emailRegex.exec(addr.label);
14 return angular.extend({}, addr, {
15 email: match ? match[2] : '(INVALID)',
16 author: match ? match[1] : '(INVALID)'
17 });
18 });
19
20 function first(array) {
21 return (array.length == 0) ? null : array[0];
22 }
23
24 return {
25 getAll: function getAll() {
26 return addrs;
27 },
28 getByAuthorEmail: function getByAuthorEmail(author, email, autocreate) {
29 var result = null;
30 _.each(addrs, function (addr) {
31 if (addr.author == author && addr.email == email) {
32 result = addr;
33 }
34 });
35 if (!result && autocreate) {
36 result = {
37 label: '(INVALID) "' + author + '" <' + email + '>',
38 author: author,
39 email: email
40 };
41 addrs.push(result);
42 }
43 return result;
44 },
45 getByEmail: function getByEmail(email) {
46 return first(_.where(addrs, {email: email}));
47 },
48 getByLabel: function (label) {
49 return first(_.where(addrs, {label: label}));
50 },
51 getDefault: function getDefault() {
52 return first(_.where(addrs, {is_default: "1"}));
53 }
54 };
55 });
56
57 angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
58 var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
59 return angular.extend({}, tpl, {
60 //id: tpl parseInt(tpl.id)
61 });
62 });
63 window.tpls = tpls;
64 var lastModifiedTpl = null;
65 return {
66 // @return Promise MessageTemplate (per APIv3)
67 get: function get(id) {
68 id = '' + id; // parseInt(id);
69 var dfr = $q.defer();
70 var tpl = _.where(tpls, {id: id});
71 if (id && tpl && tpl[0]) {
72 dfr.resolve(tpl[0]);
73 }
74 else {
75 dfr.reject(id);
76 }
77 return dfr.promise;
78 },
79 // Save a template
80 // @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
81 // @return Promise MessageTemplate (per APIv3)
82 save: function (tpl) {
83 return crmApi('MessageTemplate', 'create', tpl).then(function (response) {
84 if (!tpl.id) {
85 tpl.id = '' + response.id; //parseInt(response.id);
86 tpls.push(tpl);
87 }
88 lastModifiedTpl = tpl;
89 return tpl;
90 });
91 },
92 // @return Object MessageTemplate (per APIv3)
93 getLastModifiedTpl: function () {
94 return lastModifiedTpl;
95 },
96 getAll: function getAll() {
97 return tpls;
98 }
99 };
100 });
101
102 // The crmMailingMgr service provides business logic for loading, saving, previewing, etc
103 angular.module('crmMailing').factory('crmMailingMgr', function ($q, crmApi, crmFromAddresses, crmNow) {
104 var pickDefaultMailComponent = function pickDefaultMailComponent(type) {
105 var mcs = _.where(CRM.crmMailing.headerfooterList, {
106 component_type: type,
107 is_default: "1"
108 });
109 return (mcs.length >= 1) ? mcs[0].id : null;
110 };
111
112 return {
113 // @param scalar idExpr a number or the literal string 'new'
114 // @return Promise|Object Mailing (per APIv3)
115 getOrCreate: function getOrCreate(idExpr) {
116 return (idExpr == 'new') ? this.create() : this.get(idExpr);
117 },
118 // @return Promise Mailing (per APIv3)
119 get: function get(id) {
120 var crmMailingMgr = this;
121 var mailing;
122 return crmApi('Mailing', 'getsingle', {id: id})
123 .then(function (getResult) {
124 mailing = getResult;
125 return $q.all([
126 crmMailingMgr._loadGroups(mailing),
127 crmMailingMgr._loadJobs(mailing)
128 ]);
129 })
130 .then(function () {
131 return mailing;
132 });
133 },
134 // Call MailingGroup.get and merge results into "mailing"
135 _loadGroups: function (mailing) {
136 return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
137 .then(function (groupResult) {
138 mailing.groups = {include: [], exclude: []};
139 mailing.mailings = {include: [], exclude: []};
140 _.each(groupResult.values, function (mailingGroup) {
141 var bucket = (mailingGroup.entity_table == 'civicrm_group') ? 'groups' : 'mailings';
142 var entityId = parseInt(mailingGroup.entity_id);
143 mailing[bucket][mailingGroup.group_type].push(entityId);
144 });
145 });
146 },
147 // Call MailingJob.get and merge results into "mailing"
148 _loadJobs: function (mailing) {
149 return crmApi('MailingJob', 'get', {mailing_id: mailing.id, is_test: 0})
150 .then(function (jobResult) {
151 mailing.jobs = mailing.jobs || {};
152 angular.extend(mailing.jobs, jobResult.values);
153 });
154 },
155 // @return Object Mailing (per APIv3)
156 create: function create() {
157 return {
158 jobs: {}, // {jobId: JobRecord}
159 name: "revert this", // fixme
160 campaign_id: null,
161 from_name: crmFromAddresses.getDefault().author,
162 from_email: crmFromAddresses.getDefault().email,
163 replyto_email: "",
164 subject: "For {contact.display_name}", // fixme
165 groups: {include: [2], exclude: [4]}, // fixme
166 mailings: {include: [], exclude: []},
167 body_html: "<b>Hello</b> {contact.display_name}", // fixme
168 body_text: "Hello {contact.display_name}", // fixme
169 footer_id: null, // pickDefaultMailComponent('Footer'),
170 header_id: null, // pickDefaultMailComponent('Header'),
171 visibility: "Public Pages",
172 url_tracking: "1",
173 dedupe_email: "1",
174 forward_replies: "0",
175 auto_responder: "0",
176 open_tracking: "1",
177 override_verp: "1",
178 optout_id: pickDefaultMailComponent('OptOut'),
179 reply_id: pickDefaultMailComponent('Reply'),
180 resubscribe_id: pickDefaultMailComponent('Resubscribe'),
181 unsubscribe_id: pickDefaultMailComponent('Unsubscribe')
182 };
183 },
184
185 // @param mailing Object (per APIv3)
186 // @return Promise
187 'delete': function (mailing) {
188 if (mailing.id) {
189 return crmApi('Mailing', 'delete', {id: mailing.id});
190 }
191 else {
192 var d = $q.defer();
193 d.resolve();
194 return d.promise;
195 }
196 },
197
198 // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
199 // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
200 mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
201 var MAILING_FIELDS = [
202 // always exclude: 'id'
203 'name',
204 'campaign_id',
205 'from_name',
206 'from_email',
207 'replyto_email',
208 'subject',
209 'dedupe_email',
210 'groups',
211 'mailings',
212 'body_html',
213 'body_text',
214 'footer_id',
215 'header_id',
216 'visibility',
217 'url_tracking',
218 'dedupe_email',
219 'forward_replies',
220 'auto_responder',
221 'open_tracking',
222 'override_verp',
223 'optout_id',
224 'reply_id',
225 'resubscribe_id',
226 'unsubscribe_id'
227 ];
228 if (!excludes) {
229 excludes = [];
230 }
231 _.each(MAILING_FIELDS, function (field) {
232 if (!_.contains(excludes, field)) {
233 mailingTgt[field] = mailingFrom[field];
234 }
235 });
236 },
237
238 // @param mailing Object (per APIv3)
239 // @return Promise an object with "subject", "body_text", "body_html"
240 preview: function preview(mailing) {
241 var params = angular.extend({}, mailing, {
242 options: {force_rollback: 1},
243 'api.Mailing.preview': {
244 id: '$value.id'
245 }
246 });
247 return crmApi('Mailing', 'create', params).then(function (result) {
248 // changes rolled back, so we don't care about updating mailing
249 return result.values[result.id]['api.Mailing.preview'].values;
250 });
251 },
252
253 // @param mailing Object (per APIv3)
254 // @param int previewLimit
255 // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
256 previewRecipients: function previewRecipients(mailing, previewLimit) {
257 // To get list of recipients, we tentatively save the mailing and
258 // get the resulting recipients -- then rollback any changes.
259 var params = angular.extend({}, mailing, {
260 options: {force_rollback: 1},
261 'api.mailing_job.create': 1, // note: exact match to API default
262 'api.MailingRecipients.get': {
263 mailing_id: '$value.id',
264 options: {limit: previewLimit},
265 'api.contact.getvalue': {'return': 'display_name'},
266 'api.email.getvalue': {'return': 'email'}
267 }
268 });
269 return crmApi('Mailing', 'create', params).then(function (recipResult) {
270 // changes rolled back, so we don't care about updating mailing
271 return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
272 });
273 },
274
275 // Save a (draft) mailing
276 // @param mailing Object (per APIv3)
277 // @return Promise
278 save: function (mailing) {
279 var params = angular.extend({}, mailing, {
280 'api.mailing_job.create': 0 // note: exact match to API default
281 });
282
283 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
284 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
285 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
286 delete params.scheduled_date;
287
288 delete params.jobs;
289
290 return crmApi('Mailing', 'create', params).then(function (result) {
291 if (result.id && !mailing.id) {
292 mailing.id = result.id;
293 } // no rollback, so update mailing.id
294 // Perhaps we should reload mailing based on result?
295 return mailing;
296 });
297 },
298
299 // Schedule/send the mailing
300 // @param mailing Object (per APIv3)
301 // @return Promise
302 submit: function (mailing) {
303 var crmMailingMgr = this;
304 var params = {
305 id: mailing.id,
306 approval_date: crmNow(),
307 scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : crmNow()
308 };
309 return crmApi('Mailing', 'submit', params)
310 .then(function (result) {
311 angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
312 return crmMailingMgr._loadJobs(mailing);
313 })
314 .then(function () {
315 return mailing;
316 });
317 },
318
319 // Immediately send a test message
320 // @param mailing Object (per APIv3)
321 // @param to Object with either key "email" (string) or "gid" (int)
322 // @return Promise for a list of delivery reports
323 sendTest: function (mailing, recipient) {
324 var params = angular.extend({}, mailing, {
325 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
326 'api.Mailing.send_test': {
327 mailing_id: '$value.id',
328 test_email: recipient.email,
329 test_group: recipient.gid
330 }
331 });
332
333 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
334 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
335 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
336 delete params.scheduled_date;
337
338 delete params.jobs;
339
340 return crmApi('Mailing', 'create', params).then(function (result) {
341 if (result.id && !mailing.id) {
342 mailing.id = result.id;
343 } // no rollback, so update mailing.id
344 return result.values[result.id]['api.Mailing.send_test'].values;
345 });
346 }
347 };
348 });
349
350 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
351 angular.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService, crmMailingMgr, crmStatus) {
352 return {
353 // @param mode string one of 'html', 'text', or 'full'
354 // @return Promise
355 preview: function preview(mailing, mode) {
356 var templates = {
357 html: partialUrl('dialog/previewHtml.html'),
358 text: partialUrl('dialog/previewText.html'),
359 full: partialUrl('dialog/previewFull.html')
360 };
361 var result = null;
362 var p = crmMailingMgr
363 .preview(mailing)
364 .then(function (content) {
365 var options = {
366 autoOpen: false,
367 modal: true,
368 title: ts('Subject: %1', {
369 1: content.subject
370 })
371 };
372 result = dialogService.open('previewDialog', templates[mode], content, options);
373 });
374 crmStatus({start: ts('Previewing'), success: ''}, p);
375 return result;
376 },
377
378 // @param to Object with either key "email" (string) or "gid" (int)
379 // @return Promise
380 sendTest: function sendTest(mailing, recipient) {
381 var promise = crmMailingMgr.sendTest(mailing, recipient)
382 .then(function (deliveryInfos) {
383 var count = Object.keys(deliveryInfos).length;
384 if (count === 0) {
385 CRM.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
386 }
387 })
388 ;
389 return crmStatus({start: ts('Sending...'), success: ts('Sent')}, promise);
390 }
391 };
392 });
393
394 })(angular, CRM.$, CRM._);