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