1 (function (angular
, $, _
) {
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"
7 angular
.module('crmMailing').factory('crmFromAddresses', function ($q
, crmApi
) {
8 var emailRegex
= /^"(.*)" *<([^@>]*@[^@>]*)>$/;
9 var addrs
= _
.map(CRM
.crmMailing
.fromAddress
, function (addr
) {
10 var match
= emailRegex
.exec(addr
.label
);
11 return angular
.extend({}, addr
, {
12 email
: match
? match
[2] : '(INVALID)',
13 author
: match
? match
[1] : '(INVALID)'
17 function first(array
) {
18 return (array
.length
=== 0) ? null : array
[0];
22 getAll
: function getAll() {
25 getByAuthorEmail
: function getByAuthorEmail(author
, email
, autocreate
) {
27 _
.each(addrs
, function (addr
) {
28 if (addr
.author
== author
&& addr
.email
== email
) {
32 if (!result
&& autocreate
) {
34 label
: '(INVALID) "' + author
+ '" <' + email
+ '>',
42 getByEmail
: function getByEmail(email
) {
43 return first(_
.where(addrs
, {email
: email
}));
45 getByLabel: function (label
) {
46 return first(_
.where(addrs
, {label
: label
}));
48 getDefault
: function getDefault() {
49 return first(_
.where(addrs
, {is_default
: "1"}));
54 angular
.module('crmMailing').factory('crmMsgTemplates', function ($q
, crmApi
) {
55 var tpls
= _
.map(CRM
.crmMailing
.mesTemplate
, function (tpl
) {
56 return angular
.extend({}, tpl
, {
57 //id: tpl parseInt(tpl.id)
61 var lastModifiedTpl
= null;
64 // @param id MessageTemplate id (per APIv3)
65 // @return Promise MessageTemplate (per APIv3)
66 get: function get(id
) {
67 return crmApi('MessageTemplate', 'getsingle', {
68 "return": "id,msg_subject,msg_html,msg_title,msg_text",
73 // @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
74 // @return Promise MessageTemplate (per APIv3)
75 save: function (tpl
) {
76 return crmApi('MessageTemplate', 'create', tpl
).then(function (response
) {
78 tpl
.id
= '' + response
.id
; //parseInt(response.id);
81 lastModifiedTpl
= tpl
;
85 // @return Object MessageTemplate (per APIv3)
86 getLastModifiedTpl: function () {
87 return lastModifiedTpl
;
89 getAll
: function getAll() {
95 // The crmMailingMgr service provides business logic for loading, saving, previewing, etc
96 angular
.module('crmMailing').factory('crmMailingMgr', function ($q
, crmApi
, crmFromAddresses
, crmQueue
) {
97 var qApi
= crmQueue(crmApi
);
98 var pickDefaultMailComponent
= function pickDefaultMailComponent(type
) {
99 var mcs
= _
.where(CRM
.crmMailing
.headerfooterList
, {
100 component_type
: type
,
103 return (mcs
.length
>= 1) ? mcs
[0].id
: null;
107 // @param scalar idExpr a number or the literal string 'new'
108 // @return Promise|Object Mailing (per APIv3)
109 getOrCreate
: function getOrCreate(idExpr
) {
110 return (idExpr
== 'new') ? this.create() : this.get(idExpr
);
112 // @return Promise Mailing (per APIv3)
113 get: function get(id
) {
114 var crmMailingMgr
= this;
116 return qApi('Mailing', 'getsingle', {id
: id
})
117 .then(function (getResult
) {
120 crmMailingMgr
._loadGroups(mailing
),
121 crmMailingMgr
._loadJobs(mailing
)
128 // Call MailingGroup.get and merge results into "mailing"
129 _loadGroups: function (mailing
) {
130 return crmApi('MailingGroup', 'get', {mailing_id
: mailing
.id
, 'options': {'limit':0}})
131 .then(function (groupResult
) {
132 mailing
.recipients
= {};
133 mailing
.recipients
.groups
= {include
: [], exclude
: [], base
: []};
134 mailing
.recipients
.mailings
= {include
: [], exclude
: []};
135 _
.each(groupResult
.values
, function (mailingGroup
) {
136 var bucket
= (/^civicrm_group/.test(mailingGroup
.entity_table
)) ? 'groups' : 'mailings';
137 var entityId
= parseInt(mailingGroup
.entity_id
);
138 mailing
.recipients
[bucket
][mailingGroup
.group_type
.toLowerCase()].push(entityId
);
142 // Call MailingJob.get and merge results into "mailing"
143 _loadJobs: function (mailing
) {
144 return crmApi('MailingJob', 'get', {mailing_id
: mailing
.id
, is_test
: 0})
145 .then(function (jobResult
) {
146 mailing
.jobs
= mailing
.jobs
|| {};
147 angular
.extend(mailing
.jobs
, jobResult
.values
);
150 // @return Object Mailing (per APIv3)
151 create
: function create(params
) {
153 jobs
: {}, // {jobId: JobRecord}
155 groups
: {include
: [], exclude
: [], base
: []},
156 mailings
: {include
: [], exclude
: []}
158 template_type
: "traditional",
159 // Workaround CRM-19756 w/template_options.nonce
160 template_options
: {nonce
: 1},
168 return angular
.extend({}, defaults
, params
);
171 // @param mailing Object (per APIv3)
173 'delete': function (mailing
) {
175 return qApi('Mailing', 'delete', {id
: mailing
.id
});
184 // Search the body, header, and footer for required tokens.
185 // ex: var msgs = findMissingTokens(mailing, 'body_html');
186 findMissingTokens: function(mailing
, field
) {
188 if (!_
.isEmpty(mailing
[field
]) && !CRM
.crmMailing
.disableMandatoryTokensCheck
) {
190 if (mailing
.footer_id
) {
191 var footer
= _
.where(CRM
.crmMailing
.headerfooterList
, {id
: mailing
.footer_id
});
192 body
= body
+ footer
[0][field
];
195 body
= body
+ mailing
[field
];
196 if (mailing
.header_id
) {
197 var header
= _
.where(CRM
.crmMailing
.headerfooterList
, {id
: mailing
.header_id
});
198 body
= body
+ header
[0][field
];
201 angular
.forEach(CRM
.crmMailing
.requiredTokens
, function(value
, token
) {
202 if (!_
.isObject(value
)) {
203 if (body
.indexOf('{' + token
+ '}') < 0) {
204 missing
[token
] = value
;
209 angular
.forEach(value
, function(nestedValue
, nestedToken
) {
210 if (body
.indexOf('{' + nestedToken
+ '}') >= 0) {
215 angular
.extend(missing
, value
);
223 // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
224 // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
225 mergeInto
: function mergeInto(mailingTgt
, mailingFrom
, excludes
) {
226 var MAILING_FIELDS
= [
227 // always exclude: 'id'
255 _
.each(MAILING_FIELDS
, function (field
) {
256 if (!_
.contains(excludes
, field
)) {
257 mailingTgt
[field
] = mailingFrom
[field
];
262 // @param mailing Object (per APIv3)
263 // @return Promise an object with "subject", "body_text", "body_html"
264 preview
: function preview(mailing
) {
265 return this.getPreviewContent(qApi
, mailing
);
269 // @param mailing Object (per APIv3)
270 // @return preview content
271 getPreviewContent
: function getPreviewContent(backend
, mailing
) {
272 if (CRM
.crmMailing
.workflowEnabled
&& !CRM
.checkPerm('create mailings') && !CRM
.checkPerm('access CiviMail')) {
273 return backend('Mailing', 'preview', {id
: mailing
.id
}).then(function(result
) {
274 return result
.values
;
278 var params
= angular
.extend({}, mailing
);
280 return backend('Mailing', 'preview', params
).then(function(result
) {
281 // changes rolled back, so we don't care about updating mailing
282 return result
.values
;
287 // @param mailing Object (per APIv3)
288 // @param int previewLimit
289 // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
290 previewRecipients
: function previewRecipients(mailing
, previewLimit
) {
291 // To get list of recipients, we tentatively save the mailing and
292 // get the resulting recipients -- then rollback any changes.
293 var params
= angular
.extend({}, mailing
.recipients
, {
295 'api.MailingRecipients.get': {
296 mailing_id
: '$value.id',
297 options
: {limit
: previewLimit
},
298 'api.contact.getvalue': {'return': 'display_name'},
299 'api.email.getvalue': {'return': 'email'}
302 delete params
.scheduled_date
;
303 delete params
.recipients
; // the content was merged in
304 return qApi('Mailing', 'create', params
).then(function (recipResult
) {
305 // changes rolled back, so we don't care about updating mailing
306 mailing
.modified_date
= recipResult
.values
[recipResult
.id
].modified_date
;
307 return recipResult
.values
[recipResult
.id
]['api.MailingRecipients.get'].values
;
311 previewRecipientCount
: function previewRecipientCount(mailing
, crmMailingCache
, rebuild
) {
312 var cachekey
= 'mailing-' + mailing
.id
+ '-recipient-count';
313 var recipientCount
= crmMailingCache
.get(cachekey
);
314 if (rebuild
|| _
.isEmpty(recipientCount
)) {
315 // To get list of recipients, we tentatively save the mailing and
316 // get the resulting recipients -- then rollback any changes.
317 var params
= angular
.extend({}, mailing
, mailing
.recipients
, {
319 'api.MailingRecipients.getcount': {
320 mailing_id
: '$value.id'
323 // if this service is executed on rebuild then also fetch the recipients list
325 params
= angular
.extend(params
, {
326 'api.MailingRecipients.get': {
327 mailing_id
: '$value.id',
328 options
: {limit
: 50},
329 'api.contact.getvalue': {'return': 'display_name'},
330 'api.email.getvalue': {'return': 'email'}
333 crmMailingCache
.put('mailing-' + mailing
.id
+ '-recipient-params', params
.recipients
);
335 delete params
.scheduled_date
;
336 delete params
.recipients
; // the content was merged in
337 recipientCount
= qApi('Mailing', 'create', params
).then(function (recipResult
) {
338 // changes rolled back, so we don't care about updating mailing
339 mailing
.modified_date
= recipResult
.values
[recipResult
.id
].modified_date
;
341 crmMailingCache
.put('mailing-' + mailing
.id
+ '-recipient-list', recipResult
.values
[recipResult
.id
]['api.MailingRecipients.get'].values
);
343 return recipResult
.values
[recipResult
.id
]['api.MailingRecipients.getcount'];
345 crmMailingCache
.put(cachekey
, recipientCount
);
348 return recipientCount
;
351 // Save a (draft) mailing
352 // @param mailing Object (per APIv3)
354 save: function(mailing
) {
355 var params
= angular
.extend({}, mailing
, mailing
.recipients
);
357 // Angular ngModel sometimes treats blank fields as undefined.
358 angular
.forEach(mailing
, function(value
, key
) {
359 if (value
=== undefined || value
=== null) {
364 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
365 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
366 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
367 delete params
.scheduled_date
;
371 delete params
.recipients
; // the content was merged in
372 params
._skip_evil_bao_auto_recipients_
= 1; // skip recipient rebuild on simple save
373 return qApi('Mailing', 'create', params
).then(function(result
) {
374 if (result
.id
&& !mailing
.id
) {
375 mailing
.id
= result
.id
;
376 } // no rollback, so update mailing.id
377 // Perhaps we should reload mailing based on result?
378 mailing
.modified_date
= result
.values
[result
.id
].modified_date
;
383 // Schedule/send the mailing
384 // @param mailing Object (per APIv3)
386 submit: function (mailing
) {
387 var crmMailingMgr
= this;
390 approval_date
: 'now',
391 scheduled_date
: mailing
.scheduled_date
? mailing
.scheduled_date
: 'now'
393 return qApi('Mailing', 'submit', params
)
394 .then(function (result
) {
395 angular
.extend(mailing
, result
.values
[result
.id
]); // Perhaps we should reload mailing based on result?
396 return crmMailingMgr
._loadJobs(mailing
);
403 // Immediately send a test message
404 // @param mailing Object (per APIv3)
405 // @param to Object with either key "email" (string) or "gid" (int)
406 // @return Promise for a list of delivery reports
407 sendTest: function (mailing
, recipient
) {
408 var params
= angular
.extend({}, mailing
, mailing
.recipients
, {
409 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
410 'api.Mailing.send_test': {
411 mailing_id
: '$value.id',
412 test_email
: recipient
.email
,
413 test_group
: recipient
.gid
417 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
418 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
419 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
420 delete params
.scheduled_date
;
424 delete params
.recipients
; // the content was merged in
426 params
._skip_evil_bao_auto_recipients_
= 1; // skip recipient rebuild while sending test mail
428 return qApi('Mailing', 'create', params
).then(function (result
) {
429 if (result
.id
&& !mailing
.id
) {
430 mailing
.id
= result
.id
;
431 } // no rollback, so update mailing.id
432 mailing
.modified_date
= result
.values
[result
.id
].modified_date
;
433 return result
.values
[result
.id
]['api.Mailing.send_test'].values
;
439 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
440 angular
.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService
, crmMailingMgr
, crmStatus
) {
442 // @param mode string one of 'html', 'text', or 'full'
444 preview
: function preview(mailing
, mode
) {
446 html
: '~/crmMailing/PreviewMgr/html.html',
447 text
: '~/crmMailing/PreviewMgr/text.html',
448 full
: '~/crmMailing/PreviewMgr/full.html'
451 var p
= crmMailingMgr
452 .getPreviewContent(CRM
.api3
, mailing
)
453 .then(function (content
) {
454 var options
= CRM
.utils
.adjustDialogDefaults({
456 title
: ts('Subject: %1', {
460 result
= dialogService
.open('previewDialog', templates
[mode
], content
, options
);
462 crmStatus({start
: ts('Previewing...'), success
: ''}, p
);
466 // @param to Object with either key "email" (string) or "gid" (int)
468 sendTest
: function sendTest(mailing
, recipient
) {
469 var promise
= crmMailingMgr
.sendTest(mailing
, recipient
)
470 .then(function (deliveryInfos
) {
471 var count
= Object
.keys(deliveryInfos
).length
;
473 CRM
.alert(ts('Could not identify any recipients. Perhaps your test group is empty, or you tried sending to contacts that do not exist and you have no permission to add contacts.'));
477 return crmStatus({start
: ts('Sending...'), success
: ts('Sent')}, promise
);
482 angular
.module('crmMailing').factory('crmMailingStats', function (crmApi
, crmLegacy
) {
484 {name
: 'Recipients', title
: ts('Intended Recipients'), searchFilter
: '', eventsFilter
: '&event=queue', reportType
: 'detail', reportFilter
: ''},
485 {name
: 'Delivered', title
: ts('Successful Deliveries'), searchFilter
: '&mailing_delivery_status=Y', eventsFilter
: '&event=delivered', reportType
: 'detail', reportFilter
: '&delivery_status_value=successful'},
486 {name
: 'Opened', title
: ts('Unique Opens'), searchFilter
: '&mailing_open_status=Y', eventsFilter
: '&event=opened', reportType
: 'opened', reportFilter
: ''},
487 {name
: 'Unique Clicks', title
: ts('Unique Clicks'), searchFilter
: '&mailing_click_status=Y', eventsFilter
: '&event=click&distinct=1', reportType
: 'clicks', reportFilter
: ''},
488 // {name: 'Forward', title: ts('Forwards'), searchFilter: '&mailing_forward=1', eventsFilter: '&event=forward', reportType: 'detail', reportFilter: '&is_forwarded_value=1'},
489 // {name: 'Replies', title: ts('Replies'), searchFilter: '&mailing_reply_status=Y', eventsFilter: '&event=reply', reportType: 'detail', reportFilter: '&is_replied_value=1'},
490 {name
: 'Bounces', title
: ts('Bounces'), searchFilter
: '&mailing_delivery_status=N', eventsFilter
: '&event=bounce', reportType
: 'bounce', reportFilter
: ''},
491 {name
: 'Unsubscribers', title
: ts('Unsubscribes & Opt-outs'), searchFilter
: '&mailing_unsubscribe=1', eventsFilter
: '&event=unsubscribe', reportType
: 'detail', reportFilter
: '&is_unsubscribed_value=1'},
492 // {name: 'OptOuts', title: ts('Opt-Outs'), searchFilter: '&mailing_optout=1', eventsFilter: '&event=optout', reportType: 'detail', reportFilter: ''}
496 getStatTypes: function() {
501 * @param mailingIds object
502 * List of mailing IDs ({a: 123, b: 456})
504 * List of stats for each mailing
505 * ({a: ...object..., b: ...object...})
507 getStats: function(mailingIds
) {
509 angular
.forEach(mailingIds
, function(mailingId
, name
) {
510 params
[name
] = ['Mailing', 'stats', {mailing_id
: mailingId
, is_distinct
: 1}];
512 return crmApi(params
).then(function(result
) {
514 angular
.forEach(mailingIds
, function(mailingId
, name
) {
515 stats
[name
] = result
[name
].values
[mailingId
];
522 * Determine the legacy URL for a report about a given mailing and stat.
524 * @param mailing object
525 * @param statType object (see statTypes above)
526 * @param view string ('search', 'event', 'report')
527 * @param returnPath string|null Return path (relative to Angular base)
528 * @return string|null
530 getUrl
: function getUrl(mailing
, statType
, view
, returnPath
) {
533 var retParams
= returnPath
? '&context=angPage&angPage=' + returnPath
: '';
534 return crmLegacy
.url('civicrm/mailing/report/event',
535 'reset=1&mid=' + mailing
.id
+ statType
.eventsFilter
+ retParams
);
537 return crmLegacy
.url('civicrm/contact/search/advanced',
538 'force=1&mailing_id=' + mailing
.id
+ statType
.searchFilter
);
540 var reportIds
= CRM
.crmMailing
.reportIds
;
541 return crmLegacy
.url('civicrm/report/instance/' + reportIds
[statType
.reportType
],
542 'reset=1&mailing_id_value=' + mailing
.id
+ statType
.reportFilter
);
550 // crmMailingSimpleDirective is a template/factory-function for constructing very basic
551 // directives that accept a "mailing" argument. Please don't overload it. If one continues building
552 // this, it risks becoming a second system that violates Angular architecture (and prevents one
553 // from using standard Angular docs+plugins). So this really shouldn't do much -- it is really
554 // only for simple directives. For something complex, suck it up and write 10 lines of boilerplate.
555 angular
.module('crmMailing').factory('crmMailingSimpleDirective', function ($q
, crmMetadata
, crmUiHelp
) {
556 return function crmMailingSimpleDirective(directiveName
, templateUrl
) {
561 templateUrl
: templateUrl
,
562 link: function (scope
, elm
, attr
) {
563 scope
.$parent
.$watch(attr
.crmMailing
, function(newValue
){
564 scope
.mailing
= newValue
;
566 scope
.crmMailingConst
= CRM
.crmMailing
;
567 scope
.ts
= CRM
.ts(null);
568 scope
.hs
= crmUiHelp({file
: 'CRM/Mailing/MailingUI'});
569 scope
.checkPerm
= CRM
.checkPerm
;
570 scope
[directiveName
] = attr
[directiveName
] ? scope
.$parent
.$eval(attr
[directiveName
]) : {};
571 $q
.when(crmMetadata
.getFields('Mailing'), function(fields
) {
572 scope
.mailingFields
= fields
;
579 angular
.module('crmMailing').factory('crmMailingCache', ['$cacheFactory', function($cacheFactory
) {
580 return $cacheFactory('crmMailingCache');
583 })(angular
, CRM
.$, CRM
._
);