]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Mailing.php
Add attachments to mailings; fixes #187
[galette.git] / galette / lib / Galette / Core / Mailing.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Mailing features
7 *
8 * PHP version 5
9 *
10 * Copyright © 2009-2013 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Core
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2013 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @version SVN: $Id$
34 * @link http://galette.tuxfamily.org
35 * @since Available since 0.7dev - 2009-03-07
36 */
37
38 namespace Galette\Core;
39
40 use Analog\Analog as Analog;
41 use Galette\Entity\Adherent;
42 use Galette\IO\File;
43
44 /**
45 * Mailing features
46 *
47 * @category Core
48 * @name Mailing
49 * @package Galette
50 * @author Johan Cwiklinski <johan@x-tnd.be>
51 * @copyright 2009-2013 The Galette Team
52 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
53 * @link http://galette.tuxfamily.org
54 * @since Available since 0.7dev - 2009-03-07
55 */
56 class Mailing extends GaletteMail
57 {
58 const STEP_START = 0;
59 const STEP_PREVIEW = 1;
60 const STEP_SEND = 2;
61 const STEP_SENT = 3;
62
63 const MIME_HTML = 'text/html';
64 const MIME_TEXT = 'text/plain';
65 const MIME_DEFAULT = self::MIME_TEXT;
66
67 private $_id;
68
69 private $_unreachables;
70 private $_mrecipients;
71 private $_current_step;
72
73 private $_mime_type;
74
75 private $_tmp_path;
76 private $_history_id;
77
78 /**
79 * Default constructor
80 *
81 * @param array $members An array of members
82 * @param int $id Identifier, defaults to null
83 */
84 public function __construct($members, $id = null)
85 {
86 if ( $id !== null ) {
87 $this->_id = $id;
88 } else {
89 $this->_generateNewId();
90 }
91 $this->_current_step = self::STEP_START;
92 $this->_mime_type = self::MIME_DEFAULT;
93 /** TODO: add a preference that propose default mime-type to use,
94 then init it here */
95 if ( $members !== null) {
96 //Check which members have a valid email adress and which have not
97 $this->setRecipients($members);
98 }
99 $this->_loadAttachments();
100 }
101
102 /**
103 * Generate new mailing id
104 *
105 * @return void
106 */
107 private function _generateNewId()
108 {
109 $pass = new Password();
110 $this->_id = $pass->makeRandomPassword(30);
111 }
112
113 /**
114 * Load mailing attachments
115 *
116 * @return void
117 */
118 private function _loadAttachments()
119 {
120 $dir = '';
121 if ( isset($this->_tmp_path)
122 && trim($this->_tmp_path) !== ''
123 ) {
124 $dir = $this->_tmp_path;
125 } else {
126 $dir = GALETTE_ATTACHMENTS_PATH . $this->_id . '/';
127 }
128
129 $files = glob($dir . '*.*');
130 foreach ( $files as $file ) {
131 $f = new File($dir);
132 $f->setFileName(str_replace($dir, '', $file));
133 $this->attachments[] = $f;
134 }
135 }
136
137 /**
138 * Loads a mailing from history
139 *
140 * @param ResultSet $rs Mailing entry
141 *
142 * @return boolean
143 */
144 public function loadFromHistory($rs)
145 {
146 $orig_recipients = unserialize($rs->mailing_recipients);
147
148 $_recipients = array();
149 foreach ( $orig_recipients as $k=>$v ) {
150 $m = new Adherent($k);
151 $_recipients[] = $m;
152 }
153 $this->setRecipients($_recipients);
154 $this->subject = $rs->mailing_subject;
155 $this->message = $rs->mailing_body;
156 //if mailing has already been sent, generate a new id and copy attachments
157 if ( $rs->mailing_sent ) {
158 $this->_generateNewId();
159 $this->_copyAttachments($rs->mailing_id);
160 } else {
161 $this->_history_id = $rs->mailing_id;
162 }
163 }
164
165 /**
166 * Copy attachments from another mailing
167 *
168 * @param int $id Original mailing id
169 *
170 * @return void
171 */
172 private function _copyAttachments($id)
173 {
174 $source_dir = GALETTE_ATTACHMENTS_PATH . $id . '/';
175 $dest_dir = GALETTE_ATTACHMENTS_PATH . $this->_id . '/';
176
177 if ( file_exists($source_dir) ) {
178 if ( file_exists($dest_dir) ) {
179 throw new \RuntimeException(
180 str_replace(
181 '%s',
182 $this->_id,
183 'Attachments directory already exists for mailing %s!'
184 )
185 );
186 } else {
187 //copy attachments from source mailing and populate attachments
188 $this->attachments = array();
189 $files = glob($source_dir . '*.*');
190 foreach ( $files as $file ) {
191 $f = new File($source_dir);
192 $f->setFileName(str_replace($source_dir, '', $file));
193 $f->copyTo($dest_dir);
194 $this->attachments[] = $f;
195 }
196 }
197 } else {
198 Analog::log(
199 'No attachments in source directory',
200 Analog::DEBUG
201 );
202 }
203 }
204
205 /**
206 * Apply final header to mail and send it :-)
207 *
208 * @return GaletteMail::MAIL_ERROR|GaletteMail::MAIL_SENT
209 */
210 public function send()
211 {
212 $m = array();
213 foreach ( $this->_mrecipients as $member ) {
214 $m[$member->email] = $member->sname;
215 }
216 parent::setRecipients($m);
217 return parent::send();
218 }
219
220 /**
221 * Set mailing recipients
222 *
223 * @param array $members Array of Adherent objects
224 *
225 * @return void
226 */
227 public function setRecipients($members)
228 {
229 $m = array();
230 $this->_mrecipients = array();
231 $this->_unreachables = array();
232
233 foreach ($members as $member) {
234 $email = $member->email;
235 if ( trim($email) != '' && self::isValidEmail($email) ) {
236 if ( !in_array($member, $this->_mrecipients) ) {
237 $this->_mrecipients[] = $member;
238 }
239 $m[$email] = $member->sname;
240 } else {
241 if ( !in_array($member, $this->_unreachables) ) {
242 $this->_unreachables[] = $member;
243 }
244 }
245 }
246 parent::setRecipients($m);
247 }
248
249 /**
250 * Store maling attachments
251 *
252 * @param array $files Array of uploaded files to store
253 *
254 * @return true|int error code
255 */
256 public function store($files)
257 {
258 $this->_tmp_path = GALETTE_ATTACHMENTS_PATH . '/' . $this->_id;
259
260 if ( !file_exists($this->_tmp_path) ) {
261 //directory does not exists, create it
262 mkdir($this->_tmp_path);
263 }
264
265 if ( !is_dir($this->_tmp_path) ) {
266 throw new \RuntimeException(
267 $this->_tmp_path . ' should be a directory!'
268 );
269 }
270
271 //store files
272 $attachment = new File($this->_tmp_path);
273 $res = $attachment->store($files);
274 if ( $res < 0 ) {
275 return $res;
276 } else {
277 $this->attachments[] = $attachment;
278 }
279
280 return true;
281 }
282
283 /**
284 * Move attachments with final id once mailing has been stored
285 *
286 * @param int $id Mailing history id
287 *
288 * @return boolean
289 */
290 public function moveAttachments($id)
291 {
292 if ( isset($this->_tmp_path)
293 && trim($this->_tmp_path) !== ''
294 ) {
295 foreach ( $this->attachments as &$attachment ) {
296 $old_path = $attachment->getDestDir() . $attachment->getFileName();
297 $new_path = GALETTE_ATTACHMENTS_PATH . $this->_id .'/' . $attachment->getFileName();
298 if ( !file_exists(GALETTE_ATTACHMENTS_PATH . $this->_id) ) {
299 mkdir(GALETTE_ATTACHMENTS_PATH . $this->_id);
300 }
301 $moved = rename($old_path, $new_path);
302 if ( $moved ) {
303 $attachment->setDestDir(GALETTE_ATTACHMENTS_PATH);
304 }
305 }
306 rmdir($this->_tmp_path);
307 $this->_tmp_path = null;
308 }
309 }
310
311 /**
312 * Remove specified attachment
313 *
314 * @param string $name Filename
315 *
316 * @return void
317 */
318 public function removeAttachment($name)
319 {
320 $to_remove = null;
321 if ( isset($this->_tmp_path)
322 && trim($this->_tmp_path) !== ''
323 && file_exists($this->_tmp_path)
324 ) {
325 $to_remove = $this->_tmp_path;
326 } else if ( file_exists(GALETTE_ATTACHMENTS_PATH . $this->_id) ) {
327 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->_id;
328 }
329
330 if ( $to_remove !== null ) {
331 $to_remove .= '/' . $name;
332
333 if ( !$this->attachments ) {
334 $this->_loadAttachments();
335 }
336
337 if ( file_exists($to_remove) ) {
338 $i = 0;
339 foreach ( $this->attachments as $att ) {
340 if ( $att->getFileName() == $name ) {
341 unset($this->attachments[$i]);
342 unlink($to_remove);
343 break;
344 }
345 $i++;
346 }
347 } else {
348 Analog::log(
349 str_replace(
350 '%file',
351 $name,
352 'File %file does not exists and cannot be removed!'
353 ),
354 Analog::WARNING
355 );
356 }
357 } else {
358 throw new \RuntimeException(
359 'Unable to get attachments path!'
360 );
361 }
362 }
363
364 /**
365 * Remove mailing attachments
366 *
367 * @param boolean $temp Remove only tmporary attachments, to avoid history breaking
368 *
369 * @return void
370 */
371 public function removeAttachments($temp = false)
372 {
373 $to_remove = null;
374 if ( isset($this->_tmp_path)
375 && trim($this->_tmp_path) !== ''
376 && file_exists($this->_tmp_path)
377 ) {
378 $to_remove = $this->_tmp_path;
379 } else if ( file_exists(GALETTE_ATTACHMENTS_PATH . $this->_id) ) {
380 if ( $temp === true ) {
381 return false;
382 }
383 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->_id;
384 }
385
386 if ( $to_remove !== null ) {
387 $rdi = new \RecursiveDirectoryIterator(
388 $to_remove,
389 \FilesystemIterator::SKIP_DOTS
390 );
391 $contents = new \RecursiveIteratorIterator(
392 $rdi,
393 \RecursiveIteratorIterator::CHILD_FIRST
394 );
395 foreach ( $contents as $path) {
396 if ( $path->isFile() ) {
397 unlink($path->getPathname());
398 } else {
399 rmdir($path->getPathname());
400 }
401 }
402 rmdir($to_remove);
403 }
404 }
405
406 /**
407 * Return textual error message
408 *
409 * @param int $code The error code
410 *
411 * @return string Localized message
412 */
413 public function getAttachmentErrorMessage($code)
414 {
415 $f = new File($this->_tmp_path);
416 return $f->getErrorMessage($code);
417 }
418
419 /**
420 * Does mailing already exists in history?
421 *
422 * @return boolean
423 */
424 public function existsInHistory()
425 {
426 return isset($this->_history_id);
427 }
428
429 /**
430 * Global getter method
431 *
432 * @param string $name name of the property we want to retrive
433 *
434 * @return false|object the called property
435 */
436 public function __get($name)
437 {
438 $forbidden = array('ordered');
439 if ( !in_array($name, $forbidden) ) {
440 switch($name) {
441 case 'alt_message':
442 return $this->cleanedHtml();
443 break;
444 case 'step':
445 return $this->current_step;
446 break;
447 case 'subject':
448 return $this->getSubject();
449 break;
450 case 'message':
451 return $this->getMessage();
452 break;
453 case 'html':
454 return $this->isHTML();
455 break;
456 case 'mail':
457 case '_mail':
458 return $this->getPhpMailer();
459 break;
460 case 'errors':
461 return $this->getErrors();
462 break;
463 case 'recipients':
464 return $this->_mrecipients;
465 break;
466 case 'tmp_path':
467 if ( isset($this->_tmp_path) && trim($this->_tmp_path) !== '') {
468 return $this->_tmp_path;
469 } else {
470 //no attachments
471 return false;
472 }
473 break;
474 case 'attachments':
475 return $this->attachments;
476 break;
477 default:
478 $rname = '_' . $name;
479 Analog::log(
480 '[' . get_class($this) . 'Trying to get ' . $name .
481 ' renamed: ' . $rname,
482 Analog::DEBUG
483 );
484 return $this->$rname;
485 break;
486 }
487 } else {
488 Analog::log(
489 '[' . get_class($this) . 'Unable to get ' . $name .
490 ' renamed: ' . $rname,
491 Analog::ERROR
492 );
493 return false;
494 }
495 }
496
497 /**
498 * Global setter method
499 *
500 * @param string $name name of the property we want to assign a value to
501 * @param object $value a relevant value for the property
502 *
503 * @return void
504 */
505 public function __set($name, $value)
506 {
507 $rname = '_' . $name;
508
509 switch( $name ) {
510 case 'subject':
511 $this->setSubject($value);
512 break;
513 case 'message':
514 $this->setMessage($value);
515 break;
516 case 'html':
517 if ( is_bool($value) ) {
518 $this->isHTML($value);
519 } else {
520 Analog::log(
521 '[' . get_class($this) . '] Value for field `' . $name .
522 '` should be boolean - (' . gettype($value) . ')' .
523 $value . ' given',
524 Analog::WARNING
525 );
526 }
527 break;
528 case 'current_step':
529 if ( is_int($value)
530 && ( $value == self::STEP_START
531 || $value == self::STEP_PREVIEW
532 || $value == self::STEP_SEND
533 || $value == self::STEP_SENT )
534 ) {
535 $this->_current_step = (int)$value;
536 } else {
537 Analog::log(
538 '[' . get_class($this) . '] Value for field `' . $name .
539 '` should be integer and know - (' . gettype($value) . ')' .
540 $value . ' given',
541 Analog::WARNING
542 );
543 }
544 break;
545 case 'id':
546 $this->_id = $value;
547 break;
548 default:
549 Analog::log(
550 '[' . get_class($this) . '] Unable to set proprety `' . $name . '`',
551 Analog::WARNING
552 );
553 return false;
554 break;
555 }
556 }
557 }