1 (function (angular
, $, _
) {
2 var partialUrl = function (relPath
) {
3 return CRM
.resourceUrls
['civicrm'] + '/partials/crmMailing/' + relPath
;
6 // FIXME: surely there's already some helper which can do this in one line?
7 // @return string "YYYY-MM-DD hh:mm:ss"
8 var createNow = function () {
9 var currentdate
= new Date();
10 var yyyy
= currentdate
.getFullYear();
11 var mm
= currentdate
.getMonth() + 1;
12 mm
= mm
< 10 ? '0' + mm
: mm
;
13 var dd
= currentdate
.getDate();
14 dd
= dd
< 10 ? '0' + dd
: dd
;
15 var hh
= currentdate
.getHours();
16 hh
= hh
< 10 ? '0' + hh
: hh
;
17 var min
= currentdate
.getMinutes();
18 min
= min
< 10 ? '0' + min
: min
;
19 var sec
= currentdate
.getSeconds();
20 sec
= sec
< 10 ? '0' + sec
: sec
;
21 return yyyy
+ "-" + mm
+ "-" + dd
+ " " + hh
+ ":" + min
+ ":" + sec
;
24 // The representation of from/reply-to addresses is inconsistent in the mailing data-model,
25 // so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
26 // the available "From:" addrs. Records are like the underlying OptionValues -- but add "email"
28 angular
.module('crmMailing').factory('crmFromAddresses', function ($q
, crmApi
) {
29 var emailRegex
= /^"(.*)" \<([^@\>]*@[^@\>]*)\>$/;
30 var addrs
= _
.map(CRM
.crmMailing
.fromAddress
, function (addr
) {
31 var match
= emailRegex
.exec(addr
.label
);
32 return angular
.extend({}, addr
, {
33 email
: match
? match
[2] : '(INVALID)',
34 author
: match
? match
[1] : '(INVALID)'
38 function first(array
) {
39 return (array
.length
== 0) ? null : array
[0];
43 getAll
: function getAll() {
46 getByAuthorEmail
: function getByAuthorEmail(author
, email
, autocreate
) {
48 _
.each(addrs
, function (addr
) {
49 if (addr
.author
== author
&& addr
.email
== email
) {
53 if (!result
&& autocreate
) {
55 label
: '(INVALID) "' + author
+ '" <' + email
+ '>',
63 getByEmail
: function getByEmail(email
) {
64 return first(_
.where(addrs
, {email
: email
}));
66 getByLabel: function (label
) {
67 return first(_
.where(addrs
, {label
: label
}));
69 getDefault
: function getDefault() {
70 return first(_
.where(addrs
, {is_default
: "1"}));
75 angular
.module('crmMailing').factory('crmMsgTemplates', function ($q
, crmApi
) {
76 var tpls
= _
.map(CRM
.crmMailing
.mesTemplate
, function (tpl
) {
77 return angular
.extend({}, tpl
, {
78 //id: tpl parseInt(tpl.id)
82 var lastModifiedTpl
= null;
84 // @return Promise MessageTemplate (per APIv3)
85 get: function get(id
) {
86 id
= '' + id
; // parseInt(id);
88 var tpl
= _
.where(tpls
, {id
: id
});
89 if (id
&& tpl
&& tpl
[0]) {
98 // @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
99 // @return Promise MessageTemplate (per APIv3)
100 save: function (tpl
) {
101 return crmApi('MessageTemplate', 'create', tpl
).then(function (response
) {
103 tpl
.id
= '' + response
.id
; //parseInt(response.id);
106 lastModifiedTpl
= tpl
110 // @return Object MessageTemplate (per APIv3)
111 getLastModifiedTpl: function () {
112 return lastModifiedTpl
;
114 getAll
: function getAll() {
120 // The crmMailingMgr service provides business logic for loading, saving, previewing, etc
121 angular
.module('crmMailing').factory('crmMailingMgr', function ($q
, crmApi
, crmFromAddresses
) {
122 var pickDefaultMailComponent
= function pickDefaultMailComponent(type
) {
123 var mcs
= _
.where(CRM
.crmMailing
.headerfooterList
, {
124 component_type
: type
,
127 return (mcs
.length
>= 1) ? mcs
[0].id
: null;
131 // @param scalar idExpr a number or the literal string 'new'
132 // @return Promise|Object Mailing (per APIv3)
133 getOrCreate
: function getOrCreate(idExpr
) {
134 return (idExpr
== 'new') ? this.create() : this.get(idExpr
);
136 // @return Promise Mailing (per APIv3)
137 get: function get(id
) {
138 var crmMailingMgr
= this;
140 return crmApi('Mailing', 'getsingle', {id
: id
})
141 .then(function (getResult
) {
144 crmMailingMgr
._loadGroups(mailing
),
145 crmMailingMgr
._loadJobs(mailing
)
152 // Call MailingGroup.get and merge results into "mailing"
153 _loadGroups: function (mailing
) {
154 return crmApi('MailingGroup', 'get', {mailing_id
: mailing
.id
})
155 .then(function (groupResult
) {
156 mailing
.groups
= {include
: [], exclude
: []};
157 mailing
.mailings
= {include
: [], exclude
: []};
158 _
.each(groupResult
.values
, function (mailingGroup
) {
159 var bucket
= (mailingGroup
.entity_table
== 'civicrm_group') ? 'groups' : 'mailings';
160 var entityId
= parseInt(mailingGroup
.entity_id
);
161 mailing
[bucket
][mailingGroup
.group_type
].push(entityId
);
165 // Call MailingJob.get and merge results into "mailing"
166 _loadJobs: function (mailing
) {
167 return crmApi('MailingJob', 'get', {mailing_id
: mailing
.id
, is_test
: 0})
168 .then(function (jobResult
) {
169 mailing
.jobs
= mailing
.jobs
|| {};
170 angular
.extend(mailing
.jobs
, jobResult
.values
);
173 // @return Object Mailing (per APIv3)
174 create
: function create() {
176 jobs
: {}, // {jobId: JobRecord}
177 name
: "revert this", // fixme
179 from_name
: crmFromAddresses
.getDefault().author
,
180 from_email
: crmFromAddresses
.getDefault().email
,
182 subject
: "For {contact.display_name}", // fixme
184 groups
: {include
: [2], exclude
: [4]}, // fixme
185 mailings
: {include
: [], exclude
: []},
186 body_html
: "<b>Hello</b> {contact.display_name}", // fixme
187 body_text
: "Hello {contact.display_name}", // fixme
188 footer_id
: null, // pickDefaultMailComponent('Footer'),
189 header_id
: null, // pickDefaultMailComponent('Header'),
190 visibility
: "Public Pages",
193 forward_replies
: "0",
197 optout_id
: pickDefaultMailComponent('OptOut'),
198 reply_id
: pickDefaultMailComponent('Reply'),
199 resubscribe_id
: pickDefaultMailComponent('Resubscribe'),
200 unsubscribe_id
: pickDefaultMailComponent('Unsubscribe')
204 // @param mailing Object (per APIv3)
206 'delete': function (mailing
) {
208 return crmApi('Mailing', 'delete', {id
: mailing
.id
});
217 // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
218 // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
219 mergeInto
: function mergeInto(mailingTgt
, mailingFrom
, excludes
) {
220 var MAILING_FIELDS
= [
221 // always exclude: 'id'
250 _
.each(MAILING_FIELDS
, function (field
) {
251 if (!_
.contains(excludes
, field
)) {
252 mailingTgt
[field
] = mailingFrom
[field
];
257 // @param mailing Object (per APIv3)
258 // @return Promise an object with "subject", "body_text", "body_html"
259 preview
: function preview(mailing
) {
260 var params
= angular
.extend({}, mailing
, {
261 options
: {force_rollback
: 1},
262 'api.Mailing.preview': {
266 return crmApi('Mailing', 'create', params
).then(function (result
) {
267 // changes rolled back, so we don't care about updating mailing
268 return result
.values
[result
.id
]['api.Mailing.preview'].values
;
272 // @param mailing Object (per APIv3)
273 // @param int previewLimit
274 // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
275 previewRecipients
: function previewRecipients(mailing
, previewLimit
) {
276 // To get list of recipients, we tentatively save the mailing and
277 // get the resulting recipients -- then rollback any changes.
278 var params
= angular
.extend({}, mailing
, {
279 options
: {force_rollback
: 1},
280 'api.mailing_job.create': 1, // note: exact match to API default
281 'api.MailingRecipients.get': {
282 mailing_id
: '$value.id',
283 options
: {limit
: previewLimit
},
284 'api.contact.getvalue': {'return': 'display_name'},
285 'api.email.getvalue': {'return': 'email'}
288 return crmApi('Mailing', 'create', params
).then(function (recipResult
) {
289 // changes rolled back, so we don't care about updating mailing
290 return recipResult
.values
[recipResult
.id
]['api.MailingRecipients.get'].values
;
294 // Save a (draft) mailing
295 // @param mailing Object (per APIv3)
297 save: function (mailing
) {
298 var params
= angular
.extend({}, mailing
, {
299 'api.mailing_job.create': 0 // note: exact match to API default
302 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
303 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
304 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
305 delete params
.scheduled_date
;
309 return crmApi('Mailing', 'create', params
).then(function (result
) {
310 if (result
.id
&& !mailing
.id
) {
311 mailing
.id
= result
.id
;
312 } // no rollback, so update mailing.id
313 // Perhaps we should reload mailing based on result?
318 // Schedule/send the mailing
319 // @param mailing Object (per APIv3)
321 submit: function (mailing
) {
322 var crmMailingMgr
= this;
325 approval_date
: createNow(),
326 scheduled_date
: mailing
.scheduled_date
? mailing
.scheduled_date
: createNow()
328 return crmApi('Mailing', 'submit', params
)
329 .then(function (result
) {
330 angular
.extend(mailing
, result
.values
[result
.id
]); // Perhaps we should reload mailing based on result?
331 return crmMailingMgr
._loadJobs(mailing
);
338 // Immediately send a test message
339 // @param mailing Object (per APIv3)
340 // @param to Object with either key "email" (string) or "gid" (int)
341 // @return Promise for a list of delivery reports
342 sendTest: function (mailing
, recipient
) {
343 var params
= angular
.extend({}, mailing
, {
344 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
345 'api.Mailing.send_test': {
346 mailing_id
: '$value.id',
347 test_email
: recipient
.email
,
348 test_group
: recipient
.gid
352 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
353 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
354 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
355 delete params
.scheduled_date
;
359 return crmApi('Mailing', 'create', params
).then(function (result
) {
360 if (result
.id
&& !mailing
.id
) {
361 mailing
.id
= result
.id
;
362 } // no rollback, so update mailing.id
363 return result
.values
[result
.id
]['api.Mailing.send_test'].values
;
369 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
370 angular
.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService
, crmMailingMgr
, crmStatus
) {
372 // @param mode string one of 'html', 'text', or 'full'
374 preview
: function preview(mailing
, mode
) {
376 html
: partialUrl('dialog/previewHtml.html'),
377 text
: partialUrl('dialog/previewText.html'),
378 full
: partialUrl('dialog/previewFull.html')
381 var p
= crmMailingMgr
383 .then(function (content
) {
387 title
: ts('Subject: %1', {
391 result
= dialogService
.open('previewDialog', templates
[mode
], content
, options
);
393 crmStatus({start
: ts('Previewing'), success
: ''}, p
);
397 // @param to Object with either key "email" (string) or "gid" (int)
399 sendTest
: function sendTest(mailing
, recipient
) {
400 var promise
= crmMailingMgr
.sendTest(mailing
, recipient
)
401 .then(function (deliveryInfos
) {
402 var count
= Object
.keys(deliveryInfos
).length
;
404 CRM
.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
408 return crmStatus({start
: ts('Sending...'), success
: ts('Sent')}, promise
);
413 })(angular
, CRM
.$, CRM
._
);