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}
159 name
: "revert this", // fixme
161 from_name
: crmFromAddresses
.getDefault().author
,
162 from_email
: crmFromAddresses
.getDefault().email
,
164 subject
: "For {contact.display_name}", // fixme
165 groups
: {include
: [2], exclude
: [4]}, // fixme
166 mailings
: {include
: [], exclude
: []},
167 body_html
: "<b>Hello</b> {contact.display_name}", // fixme
168 body_text
: "Hello {contact.display_name}", // fixme
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 options
: {force_rollback
: 1},
261 'api.mailing_job.create': 1, // note: exact match to API default
262 'api.MailingRecipients.get': {
263 mailing_id
: '$value.id',
264 options
: {limit
: previewLimit
},
265 'api.contact.getvalue': {'return': 'display_name'},
266 'api.email.getvalue': {'return': 'email'}
269 return crmApi('Mailing', 'create', params
).then(function (recipResult
) {
270 // changes rolled back, so we don't care about updating mailing
271 return recipResult
.values
[recipResult
.id
]['api.MailingRecipients.get'].values
;
275 // Save a (draft) mailing
276 // @param mailing Object (per APIv3)
278 save: function (mailing
) {
279 var params
= angular
.extend({}, mailing
, {
280 'api.mailing_job.create': 0 // note: exact match to API default
283 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
284 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
285 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
286 delete params
.scheduled_date
;
290 return crmApi('Mailing', 'create', params
).then(function (result
) {
291 if (result
.id
&& !mailing
.id
) {
292 mailing
.id
= result
.id
;
293 } // no rollback, so update mailing.id
294 // Perhaps we should reload mailing based on result?
299 // Schedule/send the mailing
300 // @param mailing Object (per APIv3)
302 submit: function (mailing
) {
303 var crmMailingMgr
= this;
306 approval_date
: crmNow(),
307 scheduled_date
: mailing
.scheduled_date
? mailing
.scheduled_date
: crmNow()
309 return crmApi('Mailing', 'submit', params
)
310 .then(function (result
) {
311 angular
.extend(mailing
, result
.values
[result
.id
]); // Perhaps we should reload mailing based on result?
312 return crmMailingMgr
._loadJobs(mailing
);
319 // Immediately send a test message
320 // @param mailing Object (per APIv3)
321 // @param to Object with either key "email" (string) or "gid" (int)
322 // @return Promise for a list of delivery reports
323 sendTest: function (mailing
, recipient
) {
324 var params
= angular
.extend({}, mailing
, {
325 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
326 'api.Mailing.send_test': {
327 mailing_id
: '$value.id',
328 test_email
: recipient
.email
,
329 test_group
: recipient
.gid
333 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
334 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
335 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
336 delete params
.scheduled_date
;
340 return crmApi('Mailing', 'create', params
).then(function (result
) {
341 if (result
.id
&& !mailing
.id
) {
342 mailing
.id
= result
.id
;
343 } // no rollback, so update mailing.id
344 return result
.values
[result
.id
]['api.Mailing.send_test'].values
;
350 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
351 angular
.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService
, crmMailingMgr
, crmStatus
) {
353 // @param mode string one of 'html', 'text', or 'full'
355 preview
: function preview(mailing
, mode
) {
357 html
: partialUrl('dialog/previewHtml.html'),
358 text
: partialUrl('dialog/previewText.html'),
359 full
: partialUrl('dialog/previewFull.html')
362 var p
= crmMailingMgr
364 .then(function (content
) {
368 title
: ts('Subject: %1', {
372 result
= dialogService
.open('previewDialog', templates
[mode
], content
, options
);
374 crmStatus({start
: ts('Previewing'), success
: ''}, p
);
378 // @param to Object with either key "email" (string) or "gid" (int)
380 sendTest
: function sendTest(mailing
, recipient
) {
381 var promise
= crmMailingMgr
.sendTest(mailing
, recipient
)
382 .then(function (deliveryInfos
) {
383 var count
= Object
.keys(deliveryInfos
).length
;
385 CRM
.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
389 return crmStatus({start
: ts('Sending...'), success
: ts('Sent')}, promise
);
394 })(angular
, CRM
.$, CRM
._
);