1 (function (angular
, $, _
) {
2 var partialUrl = function (relPath
) {
3 return CRM
.resourceUrls
.civicrm
+ '/partials/crmMailing/' + relPath
;
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"
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)'
20 function first(array
) {
21 return (array
.length
=== 0) ? null : array
[0];
25 getAll
: function getAll() {
28 getByAuthorEmail
: function getByAuthorEmail(author
, email
, autocreate
) {
30 _
.each(addrs
, function (addr
) {
31 if (addr
.author
== author
&& addr
.email
== email
) {
35 if (!result
&& autocreate
) {
37 label
: '(INVALID) "' + author
+ '" <' + email
+ '>',
45 getByEmail
: function getByEmail(email
) {
46 return first(_
.where(addrs
, {email
: email
}));
48 getByLabel: function (label
) {
49 return first(_
.where(addrs
, {label
: label
}));
51 getDefault
: function getDefault() {
52 return first(_
.where(addrs
, {is_default
: "1"}));
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)
64 var lastModifiedTpl
= null;
66 // @return Promise MessageTemplate (per APIv3)
67 get: function get(id
) {
68 id
= '' + id
; // parseInt(id);
70 var tpl
= _
.where(tpls
, {id
: id
});
71 if (id
&& tpl
&& tpl
[0]) {
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
) {
85 tpl
.id
= '' + response
.id
; //parseInt(response.id);
88 lastModifiedTpl
= tpl
;
92 // @return Object MessageTemplate (per APIv3)
93 getLastModifiedTpl: function () {
94 return lastModifiedTpl
;
96 getAll
: function getAll() {
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
,
109 return (mcs
.length
>= 1) ? mcs
[0].id
: null;
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
);
118 // @return Promise Mailing (per APIv3)
119 get: function get(id
) {
120 var crmMailingMgr
= this;
122 return crmApi('Mailing', 'getsingle', {id
: id
})
123 .then(function (getResult
) {
126 crmMailingMgr
._loadGroups(mailing
),
127 crmMailingMgr
._loadJobs(mailing
)
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
);
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
);
155 // @return Object Mailing (per APIv3)
156 create
: function create() {
158 jobs
: {}, // {jobId: JobRecord}
161 from_name
: crmFromAddresses
.getDefault().author
,
162 from_email
: crmFromAddresses
.getDefault().email
,
165 groups
: {include
: [], exclude
: []},
166 mailings
: {include
: [], exclude
: []},
169 footer_id
: null, // pickDefaultMailComponent('Footer'),
170 header_id
: null, // pickDefaultMailComponent('Header'),
171 visibility
: "Public Pages",
174 forward_replies
: "0",
178 optout_id
: pickDefaultMailComponent('OptOut'),
179 reply_id
: pickDefaultMailComponent('Reply'),
180 resubscribe_id
: pickDefaultMailComponent('Resubscribe'),
181 unsubscribe_id
: pickDefaultMailComponent('Unsubscribe')
185 // @param mailing Object (per APIv3)
187 'delete': function (mailing
) {
189 return crmApi('Mailing', 'delete', {id
: mailing
.id
});
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'
231 _
.each(MAILING_FIELDS
, function (field
) {
232 if (!_
.contains(excludes
, field
)) {
233 mailingTgt
[field
] = mailingFrom
[field
];
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': {
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
;
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 name
: 'placeholder', // for previewing recipients on new, incomplete mailing
261 subject
: 'placeholder', // for previewing recipients on new, incomplete mailing
262 options
: {force_rollback
: 1},
263 'api.mailing_job.create': 1, // note: exact match to API default
264 'api.MailingRecipients.get': {
265 mailing_id
: '$value.id',
266 options
: {limit
: previewLimit
},
267 'api.contact.getvalue': {'return': 'display_name'},
268 'api.email.getvalue': {'return': 'email'}
271 return crmApi('Mailing', 'create', params
).then(function (recipResult
) {
272 // changes rolled back, so we don't care about updating mailing
273 return recipResult
.values
[recipResult
.id
]['api.MailingRecipients.get'].values
;
277 // Save a (draft) mailing
278 // @param mailing Object (per APIv3)
280 save: function (mailing
) {
281 var params
= angular
.extend({}, mailing
, {
282 'api.mailing_job.create': 0 // note: exact match to API default
285 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
286 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
287 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
288 delete params
.scheduled_date
;
292 return crmApi('Mailing', 'create', params
).then(function (result
) {
293 if (result
.id
&& !mailing
.id
) {
294 mailing
.id
= result
.id
;
295 } // no rollback, so update mailing.id
296 // Perhaps we should reload mailing based on result?
301 // Schedule/send the mailing
302 // @param mailing Object (per APIv3)
304 submit: function (mailing
) {
305 var crmMailingMgr
= this;
308 approval_date
: crmNow(),
309 scheduled_date
: mailing
.scheduled_date
? mailing
.scheduled_date
: crmNow()
311 return crmApi('Mailing', 'submit', params
)
312 .then(function (result
) {
313 angular
.extend(mailing
, result
.values
[result
.id
]); // Perhaps we should reload mailing based on result?
314 return crmMailingMgr
._loadJobs(mailing
);
321 // Immediately send a test message
322 // @param mailing Object (per APIv3)
323 // @param to Object with either key "email" (string) or "gid" (int)
324 // @return Promise for a list of delivery reports
325 sendTest: function (mailing
, recipient
) {
326 var params
= angular
.extend({}, mailing
, {
327 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
328 'api.Mailing.send_test': {
329 mailing_id
: '$value.id',
330 test_email
: recipient
.email
,
331 test_group
: recipient
.gid
335 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
336 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
337 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
338 delete params
.scheduled_date
;
342 return crmApi('Mailing', 'create', params
).then(function (result
) {
343 if (result
.id
&& !mailing
.id
) {
344 mailing
.id
= result
.id
;
345 } // no rollback, so update mailing.id
346 return result
.values
[result
.id
]['api.Mailing.send_test'].values
;
352 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
353 angular
.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService
, crmMailingMgr
, crmStatus
) {
355 // @param mode string one of 'html', 'text', or 'full'
357 preview
: function preview(mailing
, mode
) {
359 html
: partialUrl('dialog/previewHtml.html'),
360 text
: partialUrl('dialog/previewText.html'),
361 full
: partialUrl('dialog/previewFull.html')
364 var p
= crmMailingMgr
366 .then(function (content
) {
370 title
: ts('Subject: %1', {
374 result
= dialogService
.open('previewDialog', templates
[mode
], content
, options
);
376 crmStatus({start
: ts('Previewing'), success
: ''}, p
);
380 // @param to Object with either key "email" (string) or "gid" (int)
382 sendTest
: function sendTest(mailing
, recipient
) {
383 var promise
= crmMailingMgr
.sendTest(mailing
, recipient
)
384 .then(function (deliveryInfos
) {
385 var count
= Object
.keys(deliveryInfos
).length
;
387 CRM
.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
391 return crmStatus({start
: ts('Sending...'), success
: ts('Sent')}, promise
);
396 })(angular
, CRM
.$, CRM
._
);