]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Mailing.php
Prevent double load of attachments
[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 $this->_tmp_path = GALETTE_ATTACHMENTS_PATH . '/' . $this->_id;
112 }
113
114 /**
115 * Load mailing attachments
116 *
117 * @return void
118 */
119 private function _loadAttachments()
120 {
121 $dir = '';
122 if ( isset($this->_tmp_path)
123 && trim($this->_tmp_path) !== ''
124 ) {
125 $dir = $this->_tmp_path;
126 } else {
127 $dir = GALETTE_ATTACHMENTS_PATH . $this->_id . '/';
128 }
129
130 $files = glob($dir . '*.*');
131 foreach ( $files as $file ) {
132 $f = new File($dir);
133 $f->setFileName(str_replace($dir, '', $file));
134 $this->attachments[] = $f;
135 }
136 }
137
138 /**
139 * Loads a mailing from history
140 *
141 * @param ResultSet $rs Mailing entry
142 * @param boolean $new True if we create a 'new' mailing,
143 * false otherwise (from preview for example)
144 *
145 * @return boolean
146 */
147 public function loadFromHistory($rs, $new = true)
148 {
149 $orig_recipients = unserialize($rs->mailing_recipients);
150
151 $_recipients = array();
152 foreach ( $orig_recipients as $k=>$v ) {
153 $m = new Adherent($k);
154 $_recipients[] = $m;
155 }
156 $this->setRecipients($_recipients);
157 $this->subject = $rs->mailing_subject;
158 $this->message = $rs->mailing_body;
159 //if mailing has already been sent, generate a new id and copy attachments
160 if ( $rs->mailing_sent && $new ) {
161 $this->_generateNewId();
162 $this->_copyAttachments($rs->mailing_id);
163 } else {
164 $this->_tmp_path = null;
165 $this->_id = $rs->mailing_id;
166 if ( !$this->attachments ) {
167 $this->_loadAttachments();
168 }
169 $this->_history_id = $rs->mailing_id;
170 }
171 }
172
173 /**
174 * Copy attachments from another mailing
175 *
176 * @param int $id Original mailing id
177 *
178 * @return void
179 */
180 private function _copyAttachments($id)
181 {
182 $source_dir = GALETTE_ATTACHMENTS_PATH . $id . '/';
183 $dest_dir = GALETTE_ATTACHMENTS_PATH . $this->_id . '/';
184
185 if ( file_exists($source_dir) ) {
186 if ( file_exists($dest_dir) ) {
187 throw new \RuntimeException(
188 str_replace(
189 '%s',
190 $this->_id,
191 'Attachments directory already exists for mailing %s!'
192 )
193 );
194 } else {
195 //create directory
196 mkdir($dest_dir);
197 //copy attachments from source mailing and populate attachments
198 $this->attachments = array();
199 $files = glob($source_dir . '*.*');
200 foreach ( $files as $file ) {
201 $f = new File($source_dir);
202 $f->setFileName(str_replace($source_dir, '', $file));
203 $f->copyTo($dest_dir);
204 $this->attachments[] = $f;
205 }
206 }
207 } else {
208 Analog::log(
209 'No attachments in source directory',
210 Analog::DEBUG
211 );
212 }
213 }
214
215 /**
216 * Apply final header to mail and send it :-)
217 *
218 * @return GaletteMail::MAIL_ERROR|GaletteMail::MAIL_SENT
219 */
220 public function send()
221 {
222 $m = array();
223 foreach ( $this->_mrecipients as $member ) {
224 $m[$member->email] = $member->sname;
225 }
226 parent::setRecipients($m);
227 return parent::send();
228 }
229
230 /**
231 * Set mailing recipients
232 *
233 * @param array $members Array of Adherent objects
234 *
235 * @return void
236 */
237 public function setRecipients($members)
238 {
239 $m = array();
240 $this->_mrecipients = array();
241 $this->_unreachables = array();
242
243 foreach ($members as $member) {
244 $email = $member->email;
245 if ( trim($email) != '' && self::isValidEmail($email) ) {
246 if ( !in_array($member, $this->_mrecipients) ) {
247 $this->_mrecipients[] = $member;
248 }
249 $m[$email] = $member->sname;
250 } else {
251 if ( !in_array($member, $this->_unreachables) ) {
252 $this->_unreachables[] = $member;
253 }
254 }
255 }
256 parent::setRecipients($m);
257 }
258
259 /**
260 * Store maling attachments
261 *
262 * @param array $files Array of uploaded files to store
263 *
264 * @return true|int error code
265 */
266 public function store($files)
267 {
268 if ( !file_exists($this->_tmp_path) ) {
269 //directory does not exists, create it
270 mkdir($this->_tmp_path);
271 }
272
273 if ( !is_dir($this->_tmp_path) ) {
274 throw new \RuntimeException(
275 $this->_tmp_path . ' should be a directory!'
276 );
277 }
278
279 //store files
280 $attachment = new File($this->_tmp_path);
281 $res = $attachment->store($files);
282 if ( $res < 0 ) {
283 return $res;
284 } else {
285 $this->attachments[] = $attachment;
286 }
287
288 return true;
289 }
290
291 /**
292 * Move attachments with final id once mailing has been stored
293 *
294 * @param int $id Mailing history id
295 *
296 * @return boolean
297 */
298 public function moveAttachments($id)
299 {
300 if ( isset($this->_tmp_path)
301 && trim($this->_tmp_path) !== ''
302 ) {
303 foreach ( $this->attachments as &$attachment ) {
304 $old_path = $attachment->getDestDir() . $attachment->getFileName();
305 $new_path = GALETTE_ATTACHMENTS_PATH . $this->_id .'/' . $attachment->getFileName();
306 if ( !file_exists(GALETTE_ATTACHMENTS_PATH . $this->_id) ) {
307 mkdir(GALETTE_ATTACHMENTS_PATH . $this->_id);
308 }
309 $moved = rename($old_path, $new_path);
310 if ( $moved ) {
311 $attachment->setDestDir(GALETTE_ATTACHMENTS_PATH);
312 }
313 }
314 rmdir($this->_tmp_path);
315 $this->_tmp_path = null;
316 }
317 }
318
319 /**
320 * Remove specified attachment
321 *
322 * @param string $name Filename
323 *
324 * @return void
325 */
326 public function removeAttachment($name)
327 {
328 $to_remove = null;
329 if ( isset($this->_tmp_path)
330 && trim($this->_tmp_path) !== ''
331 && file_exists($this->_tmp_path)
332 ) {
333 $to_remove = $this->_tmp_path;
334 } else if ( file_exists(GALETTE_ATTACHMENTS_PATH . $this->_id) ) {
335 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->_id;
336 }
337
338 if ( $to_remove !== null ) {
339 $to_remove .= '/' . $name;
340
341 if ( !$this->attachments ) {
342 $this->_loadAttachments();
343 }
344
345 if ( file_exists($to_remove) ) {
346 $i = 0;
347 foreach ( $this->attachments as $att ) {
348 if ( $att->getFileName() == $name ) {
349 unset($this->attachments[$i]);
350 unlink($to_remove);
351 break;
352 }
353 $i++;
354 }
355 } else {
356 Analog::log(
357 str_replace(
358 '%file',
359 $name,
360 'File %file does not exists and cannot be removed!'
361 ),
362 Analog::WARNING
363 );
364 }
365 } else {
366 throw new \RuntimeException(
367 'Unable to get attachments path!'
368 );
369 }
370 }
371
372 /**
373 * Remove mailing attachments
374 *
375 * @param boolean $temp Remove only tmporary attachments, to avoid history breaking
376 *
377 * @return void
378 */
379 public function removeAttachments($temp = false)
380 {
381 $to_remove = null;
382 if ( isset($this->_tmp_path)
383 && trim($this->_tmp_path) !== ''
384 && file_exists($this->_tmp_path)
385 ) {
386 $to_remove = $this->_tmp_path;
387 } else if ( file_exists(GALETTE_ATTACHMENTS_PATH . $this->_id) ) {
388 if ( $temp === true ) {
389 return false;
390 }
391 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->_id;
392 }
393
394 if ( $to_remove !== null ) {
395 $rdi = new \RecursiveDirectoryIterator(
396 $to_remove,
397 \FilesystemIterator::SKIP_DOTS
398 );
399 $contents = new \RecursiveIteratorIterator(
400 $rdi,
401 \RecursiveIteratorIterator::CHILD_FIRST
402 );
403 foreach ( $contents as $path) {
404 if ( $path->isFile() ) {
405 unlink($path->getPathname());
406 } else {
407 rmdir($path->getPathname());
408 }
409 }
410 rmdir($to_remove);
411 }
412 }
413
414 /**
415 * Return textual error message
416 *
417 * @param int $code The error code
418 *
419 * @return string Localized message
420 */
421 public function getAttachmentErrorMessage($code)
422 {
423 $f = new File($this->_tmp_path);
424 return $f->getErrorMessage($code);
425 }
426
427 /**
428 * Does mailing already exists in history?
429 *
430 * @return boolean
431 */
432 public function existsInHistory()
433 {
434 return isset($this->_history_id);
435 }
436
437 /**
438 * Global getter method
439 *
440 * @param string $name name of the property we want to retrive
441 *
442 * @return false|object the called property
443 */
444 public function __get($name)
445 {
446 $forbidden = array('ordered');
447 if ( !in_array($name, $forbidden) ) {
448 switch($name) {
449 case 'alt_message':
450 return $this->cleanedHtml();
451 break;
452 case 'step':
453 return $this->current_step;
454 break;
455 case 'subject':
456 return $this->getSubject();
457 break;
458 case 'message':
459 return $this->getMessage();
460 break;
461 case 'html':
462 return $this->isHTML();
463 break;
464 case 'mail':
465 case '_mail':
466 return $this->getPhpMailer();
467 break;
468 case 'errors':
469 return $this->getErrors();
470 break;
471 case 'recipients':
472 return $this->_mrecipients;
473 break;
474 case 'tmp_path':
475 if ( isset($this->_tmp_path) && trim($this->_tmp_path) !== '') {
476 return $this->_tmp_path;
477 } else {
478 //no attachments
479 return false;
480 }
481 break;
482 case 'attachments':
483 return $this->attachments;
484 break;
485 default:
486 $rname = '_' . $name;
487 Analog::log(
488 '[' . get_class($this) . 'Trying to get ' . $name .
489 ' renamed: ' . $rname,
490 Analog::DEBUG
491 );
492 return $this->$rname;
493 break;
494 }
495 } else {
496 Analog::log(
497 '[' . get_class($this) . 'Unable to get ' . $name .
498 ' renamed: ' . $rname,
499 Analog::ERROR
500 );
501 return false;
502 }
503 }
504
505 /**
506 * Global setter method
507 *
508 * @param string $name name of the property we want to assign a value to
509 * @param object $value a relevant value for the property
510 *
511 * @return void
512 */
513 public function __set($name, $value)
514 {
515 $rname = '_' . $name;
516
517 switch( $name ) {
518 case 'subject':
519 $this->setSubject($value);
520 break;
521 case 'message':
522 $this->setMessage($value);
523 break;
524 case 'html':
525 if ( is_bool($value) ) {
526 $this->isHTML($value);
527 } else {
528 Analog::log(
529 '[' . get_class($this) . '] Value for field `' . $name .
530 '` should be boolean - (' . gettype($value) . ')' .
531 $value . ' given',
532 Analog::WARNING
533 );
534 }
535 break;
536 case 'current_step':
537 if ( is_int($value)
538 && ( $value == self::STEP_START
539 || $value == self::STEP_PREVIEW
540 || $value == self::STEP_SEND
541 || $value == self::STEP_SENT )
542 ) {
543 $this->_current_step = (int)$value;
544 } else {
545 Analog::log(
546 '[' . get_class($this) . '] Value for field `' . $name .
547 '` should be integer and know - (' . gettype($value) . ')' .
548 $value . ' given',
549 Analog::WARNING
550 );
551 }
552 break;
553 case 'id':
554 $this->_id = $value;
555 break;
556 default:
557 Analog::log(
558 '[' . get_class($this) . '] Unable to set proprety `' . $name . '`',
559 Analog::WARNING
560 );
561 return false;
562 break;
563 }
564 }
565 }