1 <?php
2 /* --------------------------------------------------------------
3 AttachmentsHandler.inc.php 2015-07-22 gm
4 Gambio GmbH
5 http://www.gambio.de
6 Copyright (c) 2015 Gambio GmbH
7 Released under the GNU General Public License (Version 2)
8 [http://www.gnu.org/licenses/gpl-2.0.html]
9 --------------------------------------------------------------
10 */
11
12 MainFactory::load_class('AttachmentsHandlerInterface');
13
14 /**
15 * Class AttachmentsHandler
16 *
17 * This class will handle the email attachments organization. Every email must have
18 * its own attachments directory so that we can avoid file issues between emails.
19 *
20 * @category System
21 * @package Email
22 */
23 class AttachmentsHandler implements AttachmentsHandlerInterface
24 {
25 /**
26 * Full server path to "uploads" directory.
27 *
28 * @var string
29 */
30 protected $uploadsDirPath;
31
32
33 /**
34 * Class Constructor
35 *
36 * @throws InvalidArgumentException If the provided argument is not valid.
37 *
38 * @param string $p_uploadsDirPath Path to the server's "uploads" directory. The uploads directory
39 * must already contain a "tmp" and an "attachments" directory created
40 * by an FTP client (resolves permission problems).
41 */
42 public function __construct($p_uploadsDirPath)
43 {
44 if(empty($p_uploadsDirPath) || !is_string($p_uploadsDirPath))
45 {
46 throw new InvalidArgumentException('Invalid uploads path argument provided (existing path as string expected): '
47 . gettype($p_uploadsDirPath));
48 }
49
50 if(!file_exists($p_uploadsDirPath))
51 {
52 throw new InvalidArgumentException('Provided uploads directory path does not exist in the server: '
53 . $p_uploadsDirPath);
54 }
55
56 if(!file_exists($p_uploadsDirPath . '/tmp') || !file_exists($p_uploadsDirPath . '/attachments'))
57 {
58 throw new InvalidArgumentException('Uploads directory path must contain a "tmp" and an "attachments" directory: '
59 . $p_uploadsDirPath);
60 }
61
62 $this->uploadsDirPath = rtrim((string)$p_uploadsDirPath,
63 '/\t\n\r\0\x0B'); // Remove trailing slash and other chars
64 }
65
66
67 /**
68 * Upload an attachment to "uploads/tmp" directory.
69 *
70 * This method takes the uploaded file information and places it in the "uploads/tmp" directory
71 * as a temporary place, until the "uploadEmailCollection" moves it to the final destination.
72 *
73 * @param EmailAttachmentInterface $attachment Contains the file information (path is required).
74 *
75 * @return EmailAttachment Returns an EmailAttachment instance with the new attachment path.
76 *
77 * @throws Exception If method cannot copy the file from the PHP temp dir to the destination path.
78 */
79 public function uploadAttachment(EmailAttachmentInterface $attachment)
80 {
81 $name = ($attachment->getName()
82 !== null) ? (string)$attachment->getName() : basename((string)$attachment->getPath());
83 $originalName = $name;
84 $path = $this->uploadsDirPath . '/tmp/';
85
86 // Validate uploaded file.
87 if(file_exists($path . $name))
88 {
89 // Add a counter prefix in the file name.
90 $postfixCounter = 1;
91 do
92 {
93 if(strpos($name, '.') > -1)
94 {
95 $name = preg_replace('/\.(?=[^.]*$)/', '-' . $postfixCounter . '.', $originalName);
96 }
97 else
98 {
99 $name = $originalName . '-' . $postfixCounter;
100 }
101 $postfixCounter++;
102 }
103 while(file_exists($path . $name));
104 }
105
106 // Copy file to uploads directory.
107 if(!@copy((string)$attachment->getPath(false), $path . $name))
108 {
109 throw new Exception('Could not store uploaded file: ' . (string)$attachment->getPath());
110 }
111
112 // Return new email attachment instance.
113 $newFilePath = $this->uploadsDirPath . '/tmp/' . $name;
114 $newAttachmentPath = MainFactory::create('AttachmentPath', $newFilePath);
115 $newEmailAttachment = MainFactory::create('EmailAttachment', $newAttachmentPath);
116
117 return $newEmailAttachment;
118 }
119
120
121 /**
122 * Removes a single email attachment.
123 *
124 * @param EmailAttachmentInterface $attachment E-Mail attachment.
125 */
126 public function deleteAttachment(EmailAttachmentInterface $attachment)
127 {
128 @unlink((string)$attachment->getPath());
129 }
130
131
132 /**
133 * Process attachments for each email in collection.
134 *
135 * Important! Use this method after you save the emails into the database. The reason is that
136 * this property separates each attachment file by its email ID, a value that is only accessible
137 * after the email is already saved.
138 *
139 * @param EmailCollectionInterface $collection Passed by reference, contains emails of which the
140 * attachments must be processed.
141 *
142 * @deprecated Since v2.3.3.0 this method is marked as deprecated and will be removed from the class.
143 *
144 * @codeCoverageIgnore
145 */
146 public function uploadEmailCollection(EmailCollectionInterface $collection)
147 {
148 $modifiedEmailCollection = MainFactory::create('EmailCollection'); // need to return the new collection with the changed data
149
150 foreach($collection->getArray() as $email)
151 {
152 if($email->getAttachments() !== null && count($email->getAttachments()->getArray()) > 0)
153 {
154 if($email->getId() === null)
155 {
156 throw new UnexpectedValueException('Cannot process attachments without an id.');
157 }
158
159 $attachmentsDirectory = $this->uploadsDirPath . '/attachments';
160
161 // Copy all attachments to "uploads/attachments" directory.
162 $modifiedAttachmentCollection = MainFactory::create('AttachmentCollection');
163
164 foreach($email->getAttachments()->getArray() as $attachment)
165 {
166 $oldAttachmentPath = (string)$attachment->getPath();
167 $attachmentName = (string)$attachment->getName();
168 $newAttachmentName = (!empty($attachmentName)) ? $attachmentName : basename((string)$attachment->getPath());
169 $newAttachmentPath = $attachmentsDirectory . '/email_id_' . (string)$email->getId() . '-'
170 . $newAttachmentName;
171
172 @copy($oldAttachmentPath, $newAttachmentPath);
173
174 $attachment->setPath(MainFactory::create('AttachmentPath', $newAttachmentPath));
175
176 // If attachment resides in the "tmp" directory remove it from the sever.
177 if(basename(dirname($oldAttachmentPath)) === 'tmp')
178 {
179 @unlink($oldAttachmentPath);
180 }
181
182 $modifiedAttachmentCollection->add($attachment);
183 }
184
185 $email->setAttachments($modifiedAttachmentCollection); // Replace the old attachments collection.
186 $modifiedEmailCollection->add($email); // Add the modified email instance to the emails collection.
187 }
188 $collection = $modifiedEmailCollection;
189 }
190 }
191
192
193 /**
194 * Delete attachments for each email in collection.
195 *
196 * Every email has its own attachments directory. When emails are deleted we need
197 * to remove their respective attachments.
198 *
199 * @param EmailCollectionInterface $collection Contains email records to be deleted.
200 *
201 * @deprecated Since v2.3.3.0 this method is marked as deprecated and will be removed from the class.
202 *
203 * @codeCoverageIgnore
204 */
205 public function deleteEmailCollection(EmailCollectionInterface $collection)
206 {
207 foreach($collection->getArray() as $email)
208 {
209 if($email->getAttachments() !== null)
210 {
211 foreach($email->getAttachments()->getArray() as $attachment)
212 {
213 $this->deleteAttachment((string)$attachment->getPath());
214 }
215
216 // Remove attachment directory for the email record.
217 $attachmentDirectory = $this->uploadsDirPath . '/attachments/' . (string)$email->getCreationDate()
218 ->getTimestamp();
219 if(file_exists($attachmentDirectory))
220 {
221 @rmdir($attachmentDirectory);
222 }
223 }
224 }
225 }
226
227
228 /**
229 * Get attachments directory file size in bytes.
230 *
231 * @link http://stackoverflow.com/a/21409562
232 *
233 * @return int Returns the size in bytes.
234 */
235 public function getAttachmentsSize()
236 {
237 $path = realpath($this->uploadsDirPath . '/attachments');
238 $size = 0;
239
240 if($path !== false)
241 {
242 $recursiveIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path,
243 FilesystemIterator::SKIP_DOTS));
244 foreach($recursiveIterator as $file)
245 {
246 $size += $file->getSize();
247 }
248 }
249
250 return $size;
251 }
252
253
254 /**
255 * Delete old attachments prior to removal date.
256 *
257 * This method will remove all the files and directories that are prior to the given date.
258 * It will return removal information so that user can see how much disc spaces was set free.
259 *
260 * @param DateTime $removalDate From this date and before the attachment files will be removed.
261 *
262 * @return array Returns an array which contains the "count" and "size" values or the operation.
263 */
264 public function deleteOldAttachments(DateTime $removalDate)
265 {
266 $removedAttachmentsCount = 0;
267 $removedAttachmentsSize = 0; // in bytes
268
269 if($handle = opendir($this->uploadsDirPath . '/attachments'))
270 {
271 $directoryPath = $this->uploadsDirPath . '/attachments/*';
272 $files = glob($directoryPath) ? : array();
273 foreach($files as $file)
274 {
275 if(is_file($file) && $file !== 'index.html')
276 {
277 $lastModified = filemtime($file);
278 if($lastModified <= $removalDate->getTimestamp())
279 {
280 $removedAttachmentsSize += filesize($file); // in bytes
281 unlink($file);
282 $removedAttachmentsCount++;
283 }
284 }
285 }
286
287 closedir($handle);
288 }
289
290 return array(
291 'count' => $removedAttachmentsCount,
292 'size' => $removedAttachmentsSize,
293 );
294 }
295
296
297 /**
298 * Process email attachments.
299 *
300 * This method will move all the email attachments to the "uploads/attachments" directory
301 * and store them there for future reference and history navigation purposes. The email needs
302 * to be saved first because the email ID will be used to distinguish the emails.
303 *
304 * @param EmailInterface $email Passed by reference, contains the email data.
305 */
306 public function backupEmailAttachments(EmailInterface &$email)
307 {
308 if($email->getId() === null)
309 {
310 throw new UnexpectedValueException('Cannot process attachments without an ID value. '
311 . 'You should first save the email to the database and then backup '
312 . 'its\' attachments.');
313 }
314
315 $modifiedAttachmentCollection = MainFactory::create('AttachmentCollection');
316
317 foreach($email->getAttachments()->getArray() as $attachment)
318 {
319 $oldAttachmentPath = (string)$attachment->getPath();
320 $attachmentName = (string)$attachment->getName();
321
322 $newAttachmentName = (!empty($attachmentName)) ? $attachmentName : basename($oldAttachmentPath);
323
324 // Remove existing "email_id_#" prefix from the email.
325 if(strpos($newAttachmentName, 'email_id_') !== false)
326 {
327 $sanitizedAttachmentName = preg_replace('/^.*?-/', '', $newAttachmentName);
328 if($sanitizedAttachmentName !== null)
329 {
330 $newAttachmentName = $sanitizedAttachmentName;
331 }
332 }
333
334 $newAttachmentPath = $this->uploadsDirPath . '/attachments/email_id_' . (string)$email->getId() . '-'
335 . $newAttachmentName;
336
337 @copy($oldAttachmentPath, $newAttachmentPath);
338
339 $attachment->setPath(MainFactory::create('AttachmentPath', $newAttachmentPath));
340
341 // If attachment resides in the "tmp" directory remove it from the sever.
342 if(basename(dirname($oldAttachmentPath)) === 'tmp')
343 {
344 @unlink($oldAttachmentPath);
345 }
346
347 $modifiedAttachmentCollection->add($attachment);
348 }
349 }
350
351
352 /**
353 * Deletes email attachments.
354 *
355 * This method will remove all the email attachments from the server.
356 *
357 * @param EmailInterface $email Contains the email information.
358 */
359 public function deleteEmailAttachments(EmailInterface $email)
360 {
361 foreach($email->getAttachments()->getArray() as $attachment)
362 {
363 if(file_exists((string)$attachment->getPath()))
364 {
365 @unlink((string)$attachment->getPath());
366 }
367 }
368 }
369
370
371 /**
372 * Removes all files within the "uploads/tmp" directory.
373 *
374 * There might be cases where old unused files are left within the "tmp" directory and they
375 * need to be deleted. This function will remove all these files.
376 */
377 public function emptyTempDirectory()
378 {
379 foreach(scandir($this->uploadsDirPath . '/tmp') as $filename)
380 {
381 if($filename === '.' || $filename === '..' || $filename === 'index.html')
382 {
383 continue;
384 }
385
386 @unlink($this->uploadsDirPath . '/tmp/' . $filename);
387 }
388 }
389 }