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