1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\Modules\ImportExportMailPlugin;
9:
10: use Aurora\Modules\Mail\Module as MailModule;
11:
12: /**
13: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
14: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
15: * @copyright Copyright (c) 2023, Afterlogic Corp.
16: *
17: * @property Settings $oModuleSettings
18: *
19: * @package Modules
20: */
21: class Module extends \Aurora\System\Module\AbstractModule
22: {
23: /*
24: * @var $oFilecacheManager \Aurora\System\Managers\Filecache
25: */
26: public $oFilecacheManager = null;
27: public $sLogPrefix = 'plugin-import-export-mail-';
28:
29: /**
30: * @return Module
31: */
32: public static function getInstance()
33: {
34: return parent::getInstance();
35: }
36:
37: /**
38: * @return Module
39: */
40: public static function Decorator()
41: {
42: return parent::Decorator();
43: }
44:
45: /**
46: * @return Settings
47: */
48: public function getModuleSettings()
49: {
50: return $this->oModuleSettings;
51: }
52:
53: public function init()
54: {
55: $this->aErrors = [
56: Enums\ErrorCodes::UnknownError => $this->i18N('UNKNOWN_ERROR'),
57: Enums\ErrorCodes::ErrorSizeLimit => $this->i18N('ERROR_SIZE_LIMIT', ['SIZE' => $this->oModuleSettings->UploadSizeLimitMb])
58: ];
59:
60: $this->AddEntry('transfer-mail', 'EntryTransferMail');
61: }
62:
63: /**
64: * Obtains list of module settings
65: *
66: * @return array
67: */
68: public function GetSettings()
69: {
70: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
71:
72: return [
73: 'AllowZip' => class_exists('ZipArchive'),
74: 'UploadSizeLimitMb' => $this->oModuleSettings->UploadSizeLimitMb
75: ];
76: }
77:
78: /**
79: * Creating empty files before data export
80: *
81: * @return bool|array
82: */
83: public function ExportMailPrepare()
84: {
85: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
86: $mResult = false;
87: $oUser = \Aurora\System\Api::getAuthenticatedUser();
88:
89: if ($oUser instanceof \Aurora\Modules\Core\Models\User) {
90: $sZipName = \md5(\time() . \rand(1000, 9999));
91: $mResult = [
92: 'Zip' => $sZipName,
93: ];
94: $this->getFilecacheManager()->put($oUser->PublicId, $sZipName, '', '.zip');
95: $this->getFilecacheManager()->put($oUser->PublicId, $sZipName, 'prepare', '.info');
96: }
97:
98: return $mResult;
99: }
100:
101: /**
102: * Exporting mails into zip file
103: *
104: * @param int $AccountId Mail account ID
105: * @param string $Folder Folder name
106: * @param string $Zip ZIP file name
107: * @return boolean
108: * @throws \Exception
109: */
110: public function ExportMailGenerate($AccountId, $Folder, $Zip)
111: {
112: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
113: $bResult = false;
114: $oUser = \Aurora\System\Api::getAuthenticatedUser();
115:
116: if ($oUser instanceof \Aurora\Modules\Core\Models\User) {
117: $sUserPublicId = $oUser->PublicId;
118: try {
119: $aTempFiles = [];
120:
121: $this->getFilecacheManager()->put($oUser->PublicId, $Zip, 'generate', '.info');
122: $oAccount = MailModule::Decorator()->GetAccount($AccountId);
123:
124: if ($Folder !== null && $Zip !== null
125: && $oAccount
126: && $oAccount->IdUser === $oUser->Id) {
127: $oApiMail = $this->getMailManager();
128: $iOffset = 0;
129: $iLimit = 20;
130:
131: $sZipFilePath = $this->getFilecacheManager()->generateFullFilePath($sUserPublicId, $Zip, '.zip');
132: $rZipResource = fopen($sZipFilePath, 'w+b');
133:
134: $options = new \ZipStream\Option\Archive();
135: // $options->setSendHttpHeaders(true);
136: // $options->setFlushOutput(true);
137: // $options->setContentDisposition('attachment');
138: $options->setOutputStream($rZipResource);
139:
140: $oZip = new \ZipStream\ZipStream($Zip, $options);
141: $this->Log('Start fetching mail');
142:
143: $self = $this;
144:
145: $aData = $oApiMail->getFolderInformation($oAccount, $Folder);
146: $iCount = (is_array($aData) && 4 === count($aData)) ? $aData[0] : 0;
147:
148: while ($iOffset <= $iCount) {
149: $oMessageListCollection = $oApiMail->getMessageList($oAccount, $Folder, $iOffset, $iLimit);
150: $oMessageListCollection->ForeachList(function ($oMessage) use ($oApiMail, $sUserPublicId, $oAccount, $self, $Folder, &$oZip, &$aTempFiles) {
151: $iUid = $oMessage->getUid();
152: $oApiMail->directMessageToStream(
153: $oAccount,
154: function ($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($sUserPublicId, $self, $iUid, &$oZip, &$aTempFiles) {
155: $sTempName = \md5(\time() . \rand(1000, 9999) . $sFileName);
156: $aTempFiles[] = $sTempName;
157:
158: if (is_resource($rResource) && $self->getFilecacheManager()->putFile($sUserPublicId, $sTempName, $rResource)) {
159: $sFilePath = $self->getFilecacheManager()->generateFullFilePath($sUserPublicId, $sTempName);
160: $rSubResource = fopen($sFilePath, 'rb');
161: if (is_resource($rSubResource)) {
162: $sFileName = 'uid-' . $iUid . '.eml';
163: $self->Log('Append file \'' . $sFileName . '\' to ZIP');
164: $oZip->addFileFromStream($sFileName, $rSubResource);
165: $MemoryUsage = memory_get_usage(true) / (1024 * 1024);
166: $self->Log('Memory usage: ' . $MemoryUsage);
167: @fclose($rSubResource);
168: }
169: }
170: },
171: $Folder,
172: $iUid
173: );
174: });
175: $iOffset = $iOffset + $iLimit;
176: }
177: $this->Log('End fetching mail');
178: $oZip->finish();
179: $this->Log('Create ZIP file');
180: foreach ($aTempFiles as $sTempName) {
181: $this->Log('Remove temp file: ' . $sTempName);
182: $self->getFilecacheManager()->clear($sUserPublicId, $sTempName);
183: }
184: }
185: $this->Log('Generating ZIP Result: ');
186: $this->getFilecacheManager()->put($sUserPublicId, $Zip, 'ready', '.info');
187: $bResult = true;
188: } catch (\Exception $oException) {
189: $this->getFilecacheManager()->put($sUserPublicId, $Zip, 'error', '.info');
190: $this->Log($oException, true);
191: throw $oException;
192: }
193: }
194:
195: return $bResult;
196: }
197:
198: /**
199: * Checking status of export process
200: *
201: * @param string $Zip ZIP file name
202: * @return bool|array
203: */
204: public function ExportMailStatus($Zip)
205: {
206: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
207: $mResult = false;
208: $oUser = \Aurora\System\Api::getAuthenticatedUser();
209:
210: if ($oUser instanceof \Aurora\Modules\Core\Models\User) {
211: if ($this->getFilecacheManager()->isFileExists($oUser->PublicId, $Zip, '.info')) {
212: $mResult = [
213: 'Status' => $this->getFilecacheManager()->get($oUser->PublicId, $Zip, '.info')
214: ];
215: }
216: }
217:
218: return $mResult;
219: }
220:
221: /**
222: * Entry point for mail export
223: */
224: public function EntryTransferMail()
225: {
226: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
227:
228: $sAction = (string) \Aurora\System\Router::getItemByIndex(1, '');
229: $sFileName = (string) \Aurora\System\Router::getItemByIndex(2, '');
230: if ($sAction === 'export') {
231: $this->ExportMail($sFileName);
232: }
233: }
234:
235: /**
236: * Downloading of exported emails
237: *
238: * @param string $sFileName
239: */
240: public function ExportMail($sFileName)
241: {
242: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
243: $oUser = \Aurora\System\Api::getAuthenticatedUser();
244: @ob_start();
245: ini_set('display_errors', 0);
246: if ($oUser instanceof \Aurora\Modules\Core\Models\User) {
247: $this->getFilecacheManager()->put($oUser->PublicId, $sFileName, 'download', '.info');
248: $this->Log('Start downloading ZIP file.. ');
249:
250: $sZipFilePath = $this->getFilecacheManager()->generateFullFilePath($oUser->PublicId, $sFileName, '.zip');
251: $iFileSize = filesize($sZipFilePath);
252: $this->Log('ZIP file size: ' . $iFileSize);
253:
254: header("Content-type: application/zip");
255: header("Content-Disposition: attachment; filename=export-mail.zip");
256: header('Accept-Ranges: none', true);
257: header('Content-Transfer-Encoding: binary');
258: header("Content-Length: " . $iFileSize);
259: header("Pragma: no-cache");
260: header("Expires: 0");
261:
262: $rZipResource = \fopen($sZipFilePath, 'rb');
263: if ($rZipResource !== false) {
264: $this->Log('Start write data to buffer');
265: rewind($rZipResource);
266: while (!\feof($rZipResource)) {
267: $MemoryUsage = memory_get_usage(true) / (1024 * 1024);
268: $this->Log('Write data to buffer - memory usage:' . $MemoryUsage);
269: $sBuffer = @\fread($rZipResource, 8192);
270: if (false !== $sBuffer) {
271: echo $sBuffer;
272: ob_flush();
273: flush();
274: \MailSo\Base\Utils::ResetTimeLimit();
275: continue;
276: }
277: break;
278: }
279: @\fclose($rZipResource);
280: $this->Log('End write data to buffer');
281: } else {
282: $this->Log("Error. File {$sZipFilePath} not found.");
283: }
284: $this->Log('Finish ZIP file downloading');
285: }
286: @ob_get_clean();
287: exit;
288: }
289:
290: /**
291: * Importing emails into specified folder
292: *
293: * @param int $AccountId Mail account ID
294: * @param string $Folder Folder name
295: * @param array $UploadData Information about uploaded file
296: * @return boolean
297: * @throws \Aurora\System\Exceptions\BaseException
298: * @throws \Aurora\System\Exceptions\ApiException
299: */
300: public function ImportMail($AccountId, $Folder, $UploadData)
301: {
302: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
303:
304: $bResult = false;
305: $oAccount = MailModule::Decorator()->GetAccount($AccountId);
306:
307: if ($oAccount instanceof \Aurora\Modules\Mail\Models\MailAccount && is_array($UploadData)) {
308: $oUser = \Aurora\System\Api::getAuthenticatedUser();
309: if (is_array($UploadData) && $oUser instanceof \Aurora\Modules\Core\Models\User) {
310: $iSize = (int) $UploadData['size'];
311: $iUploadSizeLimitMb = $this->oModuleSettings->UploadSizeLimitMb;
312: if ($iUploadSizeLimitMb > 0 && $iSize / (1024 * 1024) > $iUploadSizeLimitMb) {
313: throw new \Aurora\System\Exceptions\BaseException(Enums\ErrorCodes::ErrorSizeLimit);
314: }
315: $sUploadName = $UploadData['name'];
316: $bIsZipExtension = strtolower(pathinfo($sUploadName, PATHINFO_EXTENSION)) === 'zip';
317: if ($bIsZipExtension) {
318: $sSavedName = 'upload-post-' . md5($UploadData['name'] . $UploadData['tmp_name']);
319: if (is_resource($UploadData['tmp_name'])) {
320: $this->getFilecacheManager()->putFile($oUser->UUID, $sSavedName, $UploadData['tmp_name']);
321: } else {
322: $this->getFilecacheManager()->moveUploadedFile($oUser->UUID, $sSavedName, $UploadData['tmp_name']);
323: }
324: if ($this->getFilecacheManager()->isFileExists($oUser->UUID, $sSavedName)) {
325: $sSavedFullName = $this->getFilecacheManager()->generateFullFilePath($oUser->UUID, $sSavedName);
326: if (class_exists('ZipArchive')) {
327: $oZip = new \ZipArchive();
328: if ($oZip->open($sSavedFullName)) {
329: for ($i = 0; $i < $oZip->numFiles; $i++) {
330: $sFileName = $oZip->getNameIndex($i);
331: if (strtolower(pathinfo($sFileName, PATHINFO_EXTENSION)) === 'eml') {
332: $aFileParams = $oZip->statIndex($i);
333: $iStreamSize = $aFileParams['size'];
334: $rMessage = $oZip->getStream($sFileName);
335: if (is_resource($rMessage)) {
336: $this->getMailManager()->appendMessageFromStream($oAccount, $rMessage, $Folder, $iStreamSize);
337: @fclose($rMessage);
338: }
339: }
340: }
341: $oZip->close();
342: $bResult = true;
343: }
344: } else {
345: throw new \Aurora\System\Exceptions\BaseException(Enums\ErrorCodes::ZipArchiveClassNotFound);
346: }
347: } else {
348: throw new \Aurora\System\Exceptions\BaseException(Enums\ErrorCodes::UnknownError);
349: }
350: }
351: }
352: } else {
353: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
354: }
355:
356: return $bResult;
357: }
358:
359: /***private***/
360: private function getMailManager()
361: {
362: static $oMailManager = null;
363: if ($oMailManager === null) {
364: $oMailManager = new \Aurora\Modules\Mail\Managers\Main\Manager($this);
365: }
366:
367: return $oMailManager;
368: }
369:
370: private function getFilecacheManager()
371: {
372: if ($this->oFilecacheManager === null) {
373: $this->oFilecacheManager = new \Aurora\System\Managers\Filecache();
374: }
375:
376: return $this->oFilecacheManager;
377: }
378:
379: private function Log($mLog, $bIsException = false, $bIsObject = false)
380: {
381: if ($bIsException) {
382: \Aurora\System\Api::LogException($mLog, \Aurora\System\Enums\LogLevel::Full, $this->sLogPrefix);
383: } elseif ($bIsObject) {
384: \Aurora\System\Api::LogObject($mLog, \Aurora\System\Enums\LogLevel::Full, $this->sLogPrefix);
385: } else {
386: \Aurora\System\Api::Log($mLog, \Aurora\System\Enums\LogLevel::Full, $this->sLogPrefix);
387: }
388: }
389: }
390: