WIP REST API for mail: non-interactive direct sending of mails

This commit is contained in:
ralf 2023-06-30 16:33:28 +02:00
parent 2858a8a599
commit 3f760e6e72
3 changed files with 129 additions and 84 deletions

View File

@ -9,9 +9,9 @@ Implemented requests (relative to https://example.org/egroupware/groupdav.php)
<summary>Example: Querying available identities / signatures</summary> <summary>Example: Querying available identities / signatures</summary>
```bash ```bash
curl -i https://example.org/egroupware/mail --user <user> -H 'Accept: application/json' curl -i https://example.org/egroupware/groupdav.php/mail --user <user> -H 'Accept: application/json'
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8 Content-Type: application/json
{ {
"responses": { "responses": {
@ -23,7 +23,7 @@ Content-Type: application/json; charset=utf-8
``` ```
</details> </details>
- ```POST /mail[/<id>]``` send mail for default or given account <id> - ```POST /mail[/<id>]``` send mail for default or given identity <id>
<details> <details>
<summary>Example: Sending mail</summary> <summary>Example: Sending mail</summary>
@ -31,7 +31,7 @@ The content of the POST request is a JSON encoded object with following attribut
- ```to```: array of strings with (RFC882) email addresses like ```["info@egroupware.org", "Ralf Becker <rb@egroupware.org"]``` - ```to```: array of strings with (RFC882) email addresses like ```["info@egroupware.org", "Ralf Becker <rb@egroupware.org"]```
- ```cc```: array of strings with (RFC882) email addresses (optional) - ```cc```: array of strings with (RFC882) email addresses (optional)
- ```bcc```: array of strings with (RFC882) email addresses (optional) - ```bcc```: array of strings with (RFC882) email addresses (optional)
- ```replyTo```: string with (RFC822) email address (optional) - ```replyto```: string with (RFC822) email address (optional)
- ```subject```: string with subject - ```subject```: string with subject
- ```body```: string plain text body (optional) - ```body```: string plain text body (optional)
- ```bodyHtml```: string with html body (optional) - ```bodyHtml```: string with html body (optional)
@ -44,12 +44,19 @@ The content of the POST request is a JSON encoded object with following attribut
- ```shareExpiration```: "yyyy-mm-dd", default not accessed in 100 days (EPL only) - ```shareExpiration```: "yyyy-mm-dd", default not accessed in 100 days (EPL only)
- ```sharePassword```: string with password required to access share, default none (EPL only) - ```sharePassword```: string with password required to access share, default none (EPL only)
- ```folder```: folder to store send mail, default Sent folder - ```folder```: folder to store send mail, default Sent folder
- ```priority```: 1: high, 3: normal (default), 5: low
```bash ```
curl -i https://example.org/egroupware/mail --user <user> \ curl -i https://example.org/egroupware/groupdav.php/mail/ --user <user> \
-X POST -H 'Content-Type: application/json' \ -X POST -H 'Content-Type: application/json' \
--content `{"to":["info@egroupware.org"],"subject":"Testmail","body":"This is a test :)\n\nRegards"}` --data-binary '{"to":["info@egroupware.org"],"subject":"Testmail","body":"This is a test :)\n\nRegards"}'
HTTP/1.1 204 No Content HTTP/1.1 200 Ok
Content-Type: application/json
{
"status": 200,
"message": "Mail successful sent"
}
``` ```
If you are not authenticated you will get: If you are not authenticated you will get:
``` ```
@ -61,9 +68,8 @@ If there is an error sending the mail you will get:
``` ```
HTTP/1.1 500 Internal Server Error HTTP/1.1 500 Internal Server Error
Content-Type: application/json Content-Type: application/json
Content-Length: ...
{"error": 123,"message":"SMTP Server not reachable"} {"error": 500,"message":"SMTP Server not reachable"}
``` ```
</details> </details>
@ -79,21 +85,7 @@ Content-Type: application/json
{ {
"status": 200, "status": 200,
"message": "Request to open compose window sent", "message": "Request to open compose window sent"
"extra": {
"preset": {
"to": [
"Birgit Becker <bb@egroupware.org"
],
"cc": [
"info@egroupware.org"
],
"subject": "Testmail",
"body": "<pre>This is a test :)\n\nRegards</pre>",
"mimeType": "html",
"identity": "52"
}
}
} }
``` ```
- user is not online, therefore compose window can NOT be opened - user is not online, therefore compose window can NOT be opened
@ -116,7 +108,7 @@ The content of the POST request is the attachment, a Location header in the resp
to use in further requests, instead of the attachment. to use in further requests, instead of the attachment.
``` ```
curl -i https://example.org/egroupware/mail/attachment/<filename> --user <user> \ curl -i https://example.org/egroupware/groupdav.php/mail/attachment/<filename> --user <user> \
--data-binary @<file> -H 'Content-Type: <content-type-of-file>' --data-binary @<file> -H 'Content-Type: <content-type-of-file>'
HTTP/1.1 204 No Content HTTP/1.1 204 No Content
Location: https://example.org/egroupware/mail/attachment/<token> Location: https://example.org/egroupware/mail/attachment/<token>

View File

@ -1006,7 +1006,7 @@ class mail_compose
} }
if(!empty($remember)) $content = array_merge($content,$remember); if(!empty($remember)) $content = array_merge($content,$remember);
} }
foreach(array('to','cc','bcc','subject','body','mimeType') as $name) foreach(array('to','cc','bcc','subject','body','mimeType','replyto','priority') as $name)
{ {
//always handle mimeType //always handle mimeType
if ($name=='mimeType' && !empty($_REQUEST['preset'][$name])) if ($name=='mimeType' && !empty($_REQUEST['preset'][$name]))
@ -2504,7 +2504,7 @@ class mail_compose
$disableRuler = false; $disableRuler = false;
$signature = $_identity['ident_signature']; $signature = $_identity['ident_signature'];
$sigAlreadyThere = $this->mailPreferences['insertSignatureAtTopOfMessage']!='no_belowaftersend'?1:0; $sigAlreadyThere = $this->mailPreferences['insertSignatureAtTopOfMessage']!='no_belowaftersend'?1:0;
if ($sigAlreadyThere) if ($sigAlreadyThere && empty($_formData['add_signature']))
{ {
// note: if you use stationery ' s the insert signatures at the top does not apply here anymore, as the signature // note: if you use stationery ' s the insert signatures at the top does not apply here anymore, as the signature
// is already part of the body, so the signature part of the template will not be applied. // is already part of the body, so the signature part of the template will not be applied.

View File

@ -58,31 +58,24 @@ class ApiHandler extends Api\CalDAV\Handler
try { try {
if (str_starts_with($path, '/mail/attachments/')) if (str_starts_with($path, '/mail/attachments/'))
{ {
$attachment_path = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'attach--'. return self::storeAttachment($path, $options['content']);
(str_replace('/', '-', substr($options['path'], 18)) ?: 'no-name').'--');
if (file_put_contents($attachment_path, $options['content']))
{
header('Location: '.($location = '/mail/attachments/'.substr(basename($attachment_path), 8)));
echo json_encode([
'status' => 200,
'message' => 'Attachment stored',
'location' => $location,
], self::JSON_RESPONSE_OPTIONS);
return '200 Ok';
} }
throw new \Exception('Error storing attachment'); elseif (preg_match('#^/mail(/(\d+))?(/compose)?#', $path, $matches))
}
elseif (preg_match('#^/mail/((\d+):(\d+)/)?(compose)?#', $path, $matches))
{ {
$acc_id = $matches[2] ?? null; $ident_id = $matches[2] ?? self::defaultIdentity($user);
$ident_id = $matches[3] ?? null; $do_compose = (bool)($matches[3] ?? false);
$do_compose = (bool)($matches[4] ?? false);
if (!($data = json_decode($options['content'], true))) if (!($data = json_decode($options['content'], true)))
{ {
throw new \Exception('Error decoding JSON: '.json_last_error_msg(), 422); throw new \Exception('Error decoding JSON: '.json_last_error_msg(), 422);
} }
// ToDo: check required attributes // ToDo: check required attributes
$preset = array_filter(array_intersect_key($data, array_flip(['to', 'cc', 'bcc', 'replyto', 'subject', 'priority']))+[
'body' => $data['bodyHtml'] ?? null ?: $data['body'] ?? '',
'mimeType' => !empty($data['bodyHtml']) ? 'html' : 'plain',
'identity' => $ident_id,
]+self::prepareAttachments($data['attachments'] ?? [], $data['attachmentType'] ?? 'attach', $do_compose));
// for compose we need to construct a URL and push it to the client (or give an error if the client is not online) // for compose we need to construct a URL and push it to the client (or give an error if the client is not online)
if ($do_compose) if ($do_compose)
{ {
@ -91,39 +84,38 @@ class ApiHandler extends Api\CalDAV\Handler
$account_lid = Api\Accounts::id2name($user); $account_lid = Api\Accounts::id2name($user);
throw new \Exception("User '$account_lid' (#$user) is NOT online", 404); throw new \Exception("User '$account_lid' (#$user) is NOT online", 404);
} }
$extra = [
//'menuaction' => 'mail.mail_compose.compose',
'preset' => array_filter(array_intersect_key($data, array_flip(['to', 'cc', 'bcc', 'subject']))+[
'body' => $data['bodyHtml'] ?? null ?: $data['body'] ?? '',
'mimeType' => !empty($data['bodyHtml']) ? 'html' : 'plain',
'identity' => $ident_id,
]+self::prepareAttachments($data['attachments'] ?? [], $data['attachmentType'] ?? 'attach')),
];
$push = new Api\Json\Push($user); $push = new Api\Json\Push($user);
//$push->call('egw.open_link', $link, '_blank', '640x1024'); $push->call('egw.open', '', 'mail', 'add', ['preset' => $preset], '_blank', 'mail');
$push->call('egw.open', '', 'mail', 'add', $extra, '_blank', 'mail');
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode([ echo json_encode([
'status' => 200, 'status' => 200,
'message' => 'Request to open compose window sent', 'message' => 'Request to open compose window sent',
'extra' => $extra, //'data' => $preset,
], self::JSON_RESPONSE_OPTIONS); ], self::JSON_RESPONSE_OPTIONS);
return true; return true;
} }
$compose = new mail_compose($acc_id); $compose = new \mail_compose($acc_id=Api\Mail\Account::read_identity($ident_id)['acc_id']);
$compose->compose([ $preset = array_filter([
'mailaccount' => $acc_id.':'.$ident_id, 'mailaccount' => $acc_id,
'mail_plaintext' => $data['body'] ?? null, 'mailidentity' => $ident_id,
'mail_htmltext' => $data['bodyHtml'] ?? null, 'identity' => null,
'mimeType' => !empty($data['bodyHtml']) ? 'html' : 'plain', 'add_signature' => true, // add signature in send, independent what preference says
'file' => array_map(__CLASS__.'::uploadsForAttachments', $data['attachments'] ?? []), ]+$preset);
]+array_diff_key($data+array_flip(['attachments', 'body', 'bodyHtml']))); if ($compose->send($preset))
{
header('Content-Type: application/json');
echo json_encode([
'status' => 200,
'message' => 'Mail successful sent',
//'data' => $preset,
], self::JSON_RESPONSE_OPTIONS);
return true;
}
throw new \Exception($compose->error_info);
} }
header('Content-Type: application/json'); throw new \Exception('Not Found', 404);
echo $options['content'];
return true;
} }
catch (\Throwable $e) { catch (\Throwable $e) {
_egw_log_exception($e); _egw_log_exception($e);
@ -142,15 +134,64 @@ class ApiHandler extends Api\CalDAV\Handler
} }
} }
/**
* Store uploaded attachment and return token
*
* @param string $path
* @param string|stream $content
* @return string HTTP status
* @throws \Exception on error
*/
protected static function storeAttachment(string $path, $content)
{
$attachment_path = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'attach--'.
(str_replace('/', '-', substr($path, 18)) ?: 'no-name').'--');
if (file_put_contents($attachment_path, $content))
{
header('Location: '.($location = '/mail/attachments/'.substr(basename($attachment_path), 8)));
echo json_encode([
'status' => 200,
'message' => 'Attachment stored',
'location' => $location,
], self::JSON_RESPONSE_OPTIONS);
return '200 Ok';
}
throw new \Exception('Error storing attachment');
}
/**
* Get default identity of user
*
* @param int $user
* @return int ident_id
* @throws Api\Exception\WrongParameter
* @throws \Exception (404) if user has no IMAP account
*/
protected static function defaultIdentity(int $user)
{
foreach(Api\Mail\Account::search($user,false) as $acc_id => $account)
{
// do NOT add SMTP only accounts as identities
if (!$account->is_imap(false)) continue;
foreach($account->identities($acc_id) as $ident_id => $identity)
{
return $ident_id;
}
}
throw new \Exception("No IMAP account found for user #$user", 404);
}
/** /**
* Convert an attachment name into an upload array for mail_compose::compose * Convert an attachment name into an upload array for mail_compose::compose
* *
* @param string[]? $attachments either "/mail/attachments/<token>" / file in temp_dir or VFS path * @param string[]? $attachments either "/mail/attachments/<token>" / file in temp_dir or VFS path
* @param string? $attachmentType "attach" (default), "link", "share_ro", "share_rw" * @param string? $attachmentType "attach" (default), "link", "share_ro", "share_rw"
* @param bool $compose true: for compose window, false: to send
* @return array with values for keys "file", "name" and "filemode" * @return array with values for keys "file", "name" and "filemode"
* @throws Exception if file not found or unreadable * @throws Exception if file not found or unreadable
*/ */
protected static function prepareAttachments(array $attachments, string $attachmentType=null) protected static function prepareAttachments(array $attachments, string $attachmentType=null, bool $compose=true)
{ {
$ret = []; $ret = [];
foreach($attachments as $attachment) foreach($attachments as $attachment)
@ -161,14 +202,20 @@ class ApiHandler extends Api\CalDAV\Handler
{ {
throw new \Exception("Attachment $attachment NOT found", 400); throw new \Exception("Attachment $attachment NOT found", 400);
} }
if ($compose)
{
$ret['file'][] = $path; $ret['file'][] = $path;
$ret['name'][] = $matches[2]; $ret['name'][] = $matches[2];
/*return [ }
else
{
$ret['attachments'][] = [
'name' => $matches[2], 'name' => $matches[2],
'type' => Api\Vfs::mime_content_type($path), 'type' => Api\Vfs::mime_content_type($path),
'file' => $path, 'file' => $path,
'size' => filesize($path), 'size' => filesize($path),
];*/ ];
}
} }
else else
{ {
@ -176,14 +223,20 @@ class ApiHandler extends Api\CalDAV\Handler
{ {
throw new \Exception("Attachment $attachment NOT found", 400); throw new \Exception("Attachment $attachment NOT found", 400);
} }
if ($compose)
{
$ret['file'][] = Api\Vfs::PREFIX.$attachment; $ret['file'][] = Api\Vfs::PREFIX.$attachment;
$ret['name'][] = Api\Vfs::basename($attachment); $ret['name'][] = Api\Vfs::basename($attachment);
/*return [ }
else
{
$ret['attachments'][] = [
'name' => Api\Vfs::basename($attachment), 'name' => Api\Vfs::basename($attachment),
'type' => Api\Vfs::mime_content_type($attachment), 'type' => Api\Vfs::mime_content_type($attachment),
'file' => Api\Vfs::PREFIX.$attachment, 'file' => Api\Vfs::PREFIX.$attachment,
'size' => filesize(Api\Vfs::PREFIX.$attachment), 'size' => filesize(Api\Vfs::PREFIX.$attachment),
];*/ ];
}
} }
} }
if ($ret) if ($ret)