Merge branch 'master' into master-civimail-abtest
[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 dedupe_email: "1",
166 groups: {include: [2], exclude: [4]}, // fixme
167 mailings: {include: [], exclude: []},
168 body_html: "<b>Hello</b> {contact.display_name}", // fixme
169 body_text: "Hello {contact.display_name}", // fixme
170 footer_id: null, // pickDefaultMailComponent('Footer'),
171 header_id: null, // pickDefaultMailComponent('Header'),
172 visibility: "Public Pages",
173 url_tracking: "1",
174 dedupe_email: "1",
175 forward_replies: "0",
176 auto_responder: "0",
177 open_tracking: "1",
178 override_verp: "1",
179 optout_id: pickDefaultMailComponent('OptOut'),
180 reply_id: pickDefaultMailComponent('Reply'),
181 resubscribe_id: pickDefaultMailComponent('Resubscribe'),
182 unsubscribe_id: pickDefaultMailComponent('Unsubscribe')
183 };
184 },
185
186 // @param mailing Object (per APIv3)
187 // @return Promise
188 'delete': function (mailing) {
189 if (mailing.id) {
190 return crmApi('Mailing', 'delete', {id: mailing.id});
191 }
192 else {
193 var d = $q.defer();
194 d.resolve();
195 return d.promise;
196 }
197 },
198
199 // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
200 // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
201 mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
202 var MAILING_FIELDS = [
203 // always exclude: 'id'
204 'name',
205 'campaign_id',
206 'from_name',
207 'from_email',
208 'replyto_email',
209 'subject',
210 'dedupe_email',
211 'groups',
212 'mailings',
213 'body_html',
214 'body_text',
215 'footer_id',
216 'header_id',
217 'visibility',
218 'url_tracking',
219 'dedupe_email',
220 'forward_replies',
221 'auto_responder',
222 'open_tracking',
223 'override_verp',
224 'optout_id',
225 'reply_id',
226 'resubscribe_id',
227 'unsubscribe_id'
228 ];
229 if (!excludes) {
230 excludes = [];
231 }
232 _.each(MAILING_FIELDS, function (field) {
233 if (!_.contains(excludes, field)) {
234 mailingTgt[field] = mailingFrom[field];
235 }
236 })
237 },
238
239 // @param mailing Object (per APIv3)
240 // @return Promise an object with "subject", "body_text", "body_html"
241 preview: function preview(mailing) {
242 var params = angular.extend({}, mailing, {
243 options: {force_rollback: 1},
244 'api.Mailing.preview': {
245 id: '$value.id'
246 }
247 });
248 return crmApi('Mailing', 'create', params).then(function (result) {
249 // changes rolled back, so we don't care about updating mailing
250 return result.values[result.id]['api.Mailing.preview'].values;
251 });
252 },
253
254 // @param mailing Object (per APIv3)
255 // @param int previewLimit
256 // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
257 previewRecipients: function previewRecipients(mailing, previewLimit) {
258 // To get list of recipients, we tentatively save the mailing and
259 // get the resulting recipients -- then rollback any changes.
260 var params = angular.extend({}, mailing, {
261 options: {force_rollback: 1},
262 'api.mailing_job.create': 1, // note: exact match to API default
263 'api.MailingRecipients.get': {
264 mailing_id: '$value.id',
265 options: {limit: previewLimit},
266 'api.contact.getvalue': {'return': 'display_name'},
267 'api.email.getvalue': {'return': 'email'}
268 }
269 });
270 return crmApi('Mailing', 'create', params).then(function (recipResult) {
271 // changes rolled back, so we don't care about updating mailing
272 return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
273 });
274 },
275
276 // Save a (draft) mailing
277 // @param mailing Object (per APIv3)
278 // @return Promise
279 save: function (mailing) {
280 var params = angular.extend({}, mailing, {
281 'api.mailing_job.create': 0 // note: exact match to API default
282 });
283
284 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
285 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
286 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
287 delete params.scheduled_date;
288
289 delete params.jobs;
290
291 return crmApi('Mailing', 'create', params).then(function (result) {
292 if (result.id && !mailing.id) {
293 mailing.id = result.id;
294 } // no rollback, so update mailing.id
295 // Perhaps we should reload mailing based on result?
296 return mailing;
297 });
298 },
299
300 // Schedule/send the mailing
301 // @param mailing Object (per APIv3)
302 // @return Promise
303 submit: function (mailing) {
304 var crmMailingMgr = this;
305 var params = {
306 id: mailing.id,
307 approval_date: crmNow(),
308 scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : crmNow()
309 };
310 return crmApi('Mailing', 'submit', params)
311 .then(function (result) {
312 angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
313 return crmMailingMgr._loadJobs(mailing);
314 })
315 .then(function () {
316 return mailing;
317 });
318 },
319
320 // Immediately send a test message
321 // @param mailing Object (per APIv3)
322 // @param to Object with either key "email" (string) or "gid" (int)
323 // @return Promise for a list of delivery reports
324 sendTest: function (mailing, recipient) {
325 var params = angular.extend({}, mailing, {
326 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
327 'api.Mailing.send_test': {
328 mailing_id: '$value.id',
329 test_email: recipient.email,
330 test_group: recipient.gid
331 }
332 });
333
334 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
335 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
336 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
337 delete params.scheduled_date;
338
339 delete params.jobs;
340
341 return crmApi('Mailing', 'create', params).then(function (result) {
342 if (result.id && !mailing.id) {
343 mailing.id = result.id;
344 } // no rollback, so update mailing.id
345 return result.values[result.id]['api.Mailing.send_test'].values;
346 });
347 }
348 };
349 });
350
351 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
352 angular.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService, crmMailingMgr, crmStatus) {
353 return {
354 // @param mode string one of 'html', 'text', or 'full'
355 // @return Promise
356 preview: function preview(mailing, mode) {
357 var templates = {
358 html: partialUrl('dialog/previewHtml.html'),
359 text: partialUrl('dialog/previewText.html'),
360 full: partialUrl('dialog/previewFull.html')
361 };
362 var result = null;
363 var p = crmMailingMgr
364 .preview(mailing)
365 .then(function (content) {
366 var options = {
367 autoOpen: false,
368 modal: true,
369 title: ts('Subject: %1', {
370 1: content.subject
371 })
372 };
373 result = dialogService.open('previewDialog', templates[mode], content, options);
374 });
375 crmStatus({start: ts('Previewing'), success: ''}, p);
376 return result;
377 },
378
379 // @param to Object with either key "email" (string) or "gid" (int)
380 // @return Promise
381 sendTest: function sendTest(mailing, recipient) {
382 var promise = crmMailingMgr.sendTest(mailing, recipient)
383 .then(function (deliveryInfos) {
384 var count = Object.keys(deliveryInfos).length;
385 if (count === 0) {
386 CRM.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
387 }
388 })
389 ;
390 return crmStatus({start: ts('Sending...'), success: ts('Sent')}, promise);
391 }
392 };
393 });
394
395 })(angular, CRM.$, CRM._);