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;
63 // @return Promise MessageTemplate (per APIv3)
64 get: function get(id
) {
65 id
= '' + id
; // parseInt(id);
67 var tpl
= _
.where(tpls
, {id
: id
});
68 if (id
&& tpl
&& tpl
[0]) {
77 // @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
78 // @return Promise MessageTemplate (per APIv3)
79 save: function (tpl
) {
80 return crmApi('MessageTemplate', 'create', tpl
).then(function (response
) {
82 tpl
.id
= '' + response
.id
; //parseInt(response.id);
85 lastModifiedTpl
= tpl
;
89 // @return Object MessageTemplate (per APIv3)
90 getLastModifiedTpl: function () {
91 return lastModifiedTpl
;
93 getAll
: function getAll() {
99 // The crmMailingMgr service provides business logic for loading, saving, previewing, etc
100 angular
.module('crmMailing').factory('crmMailingMgr', function ($q
, crmApi
, crmFromAddresses
, crmNow
) {
101 var pickDefaultMailComponent
= function pickDefaultMailComponent(type
) {
102 var mcs
= _
.where(CRM
.crmMailing
.headerfooterList
, {
103 component_type
: type
,
106 return (mcs
.length
>= 1) ? mcs
[0].id
: null;
110 // @param scalar idExpr a number or the literal string 'new'
111 // @return Promise|Object Mailing (per APIv3)
112 getOrCreate
: function getOrCreate(idExpr
) {
113 return (idExpr
== 'new') ? this.create() : this.get(idExpr
);
115 // @return Promise Mailing (per APIv3)
116 get: function get(id
) {
117 var crmMailingMgr
= this;
119 return crmApi('Mailing', 'getsingle', {id
: id
})
120 .then(function (getResult
) {
123 crmMailingMgr
._loadGroups(mailing
),
124 crmMailingMgr
._loadJobs(mailing
)
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
) {
135 mailing
.groups
= {include
: [], exclude
: []};
136 mailing
.mailings
= {include
: [], exclude
: []};
137 _
.each(groupResult
.values
, function (mailingGroup
) {
138 var bucket
= (mailingGroup
.entity_table
== 'civicrm_group') ? 'groups' : 'mailings';
139 var entityId
= parseInt(mailingGroup
.entity_id
);
140 mailing
[bucket
][mailingGroup
.group_type
].push(entityId
);
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
);
152 // @return Object Mailing (per APIv3)
153 create
: function create() {
155 jobs
: {}, // {jobId: JobRecord}
158 from_name
: crmFromAddresses
.getDefault().author
,
159 from_email
: crmFromAddresses
.getDefault().email
,
161 subject
: "New Mailing",
162 groups
: {include
: [], exclude
: []},
163 mailings
: {include
: [], exclude
: []},
166 footer_id
: null, // pickDefaultMailComponent('Footer'),
167 header_id
: null, // pickDefaultMailComponent('Header'),
168 visibility
: "Public Pages",
171 forward_replies
: "0",
175 optout_id
: pickDefaultMailComponent('OptOut'),
176 reply_id
: pickDefaultMailComponent('Reply'),
177 resubscribe_id
: pickDefaultMailComponent('Resubscribe'),
178 unsubscribe_id
: pickDefaultMailComponent('Unsubscribe')
182 // @param mailing Object (per APIv3)
184 'delete': function (mailing
) {
186 return crmApi('Mailing', 'delete', {id
: mailing
.id
});
195 // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
196 // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
197 mergeInto
: function mergeInto(mailingTgt
, mailingFrom
, excludes
) {
198 var MAILING_FIELDS
= [
199 // always exclude: 'id'
228 _
.each(MAILING_FIELDS
, function (field
) {
229 if (!_
.contains(excludes
, field
)) {
230 mailingTgt
[field
] = mailingFrom
[field
];
235 // @param mailing Object (per APIv3)
236 // @return Promise an object with "subject", "body_text", "body_html"
237 preview
: function preview(mailing
) {
238 var params
= angular
.extend({}, mailing
, {
239 options
: {force_rollback
: 1},
240 'api.Mailing.preview': {
244 return crmApi('Mailing', 'create', params
).then(function (result
) {
245 // changes rolled back, so we don't care about updating mailing
246 return result
.values
[result
.id
]['api.Mailing.preview'].values
;
250 // @param mailing Object (per APIv3)
251 // @param int previewLimit
252 // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
253 previewRecipients
: function previewRecipients(mailing
, previewLimit
) {
254 // To get list of recipients, we tentatively save the mailing and
255 // get the resulting recipients -- then rollback any changes.
256 var params
= angular
.extend({}, mailing
, {
257 name
: 'placeholder', // for previewing recipients on new, incomplete mailing
258 subject
: 'placeholder', // for previewing recipients on new, incomplete mailing
259 options
: {force_rollback
: 1},
260 'api.mailing_job.create': 1, // note: exact match to API default
261 'api.MailingRecipients.get': {
262 mailing_id
: '$value.id',
263 options
: {limit
: previewLimit
},
264 'api.contact.getvalue': {'return': 'display_name'},
265 'api.email.getvalue': {'return': 'email'}
268 return crmApi('Mailing', 'create', params
).then(function (recipResult
) {
269 // changes rolled back, so we don't care about updating mailing
270 return recipResult
.values
[recipResult
.id
]['api.MailingRecipients.get'].values
;
274 // Save a (draft) mailing
275 // @param mailing Object (per APIv3)
277 save: function (mailing
) {
278 var params
= angular
.extend({}, mailing
, {
279 'api.mailing_job.create': 0 // note: exact match to API default
282 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
283 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
284 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
285 delete params
.scheduled_date
;
289 return crmApi('Mailing', 'create', params
).then(function (result
) {
290 if (result
.id
&& !mailing
.id
) {
291 mailing
.id
= result
.id
;
292 } // no rollback, so update mailing.id
293 // Perhaps we should reload mailing based on result?
298 // Schedule/send the mailing
299 // @param mailing Object (per APIv3)
301 submit: function (mailing
) {
302 var crmMailingMgr
= this;
305 approval_date
: crmNow(),
306 scheduled_date
: mailing
.scheduled_date
? mailing
.scheduled_date
: crmNow()
308 return crmApi('Mailing', 'submit', params
)
309 .then(function (result
) {
310 angular
.extend(mailing
, result
.values
[result
.id
]); // Perhaps we should reload mailing based on result?
311 return crmMailingMgr
._loadJobs(mailing
);
318 // Immediately send a test message
319 // @param mailing Object (per APIv3)
320 // @param to Object with either key "email" (string) or "gid" (int)
321 // @return Promise for a list of delivery reports
322 sendTest: function (mailing
, recipient
) {
323 var params
= angular
.extend({}, mailing
, {
324 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
325 'api.Mailing.send_test': {
326 mailing_id
: '$value.id',
327 test_email
: recipient
.email
,
328 test_group
: recipient
.gid
332 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
333 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
334 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
335 delete params
.scheduled_date
;
339 return crmApi('Mailing', 'create', params
).then(function (result
) {
340 if (result
.id
&& !mailing
.id
) {
341 mailing
.id
= result
.id
;
342 } // no rollback, so update mailing.id
343 return result
.values
[result
.id
]['api.Mailing.send_test'].values
;
349 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
350 angular
.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService
, crmMailingMgr
, crmStatus
) {
352 // @param mode string one of 'html', 'text', or 'full'
354 preview
: function preview(mailing
, mode
) {
356 html
: '~/crmMailing/dialog/previewHtml.html',
357 text
: '~/crmMailing/dialog/previewText.html',
358 full
: '~/crmMailing/dialog/previewFull.html'
361 var p
= crmMailingMgr
363 .then(function (content
) {
367 title
: ts('Subject: %1', {
371 result
= dialogService
.open('previewDialog', templates
[mode
], content
, options
);
373 crmStatus({start
: ts('Previewing'), success
: ''}, p
);
377 // @param to Object with either key "email" (string) or "gid" (int)
379 sendTest
: function sendTest(mailing
, recipient
) {
380 var promise
= crmMailingMgr
.sendTest(mailing
, recipient
)
381 .then(function (deliveryInfos
) {
382 var count
= Object
.keys(deliveryInfos
).length
;
384 CRM
.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
388 return crmStatus({start
: ts('Sending...'), success
: ts('Sent')}, promise
);
393 })(angular
, CRM
.$, CRM
._
);