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 4616fb03d0
commit 8a3fd670ee
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>
```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
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"responses": {
@ -23,7 +23,7 @@ Content-Type: application/json; charset=utf-8
```
</details>
- ```POST /mail[/<id>]``` send mail for default or given account <id>
- ```POST /mail[/<id>]``` send mail for default or given identity <id>
<details>
<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"]```
- ```cc```: 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
- ```body```: string plain text 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)
- ```sharePassword```: string with password required to access share, default none (EPL only)
- ```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' \
--content `{"to":["info@egroupware.org"],"subject":"Testmail","body":"This is a test :)\n\nRegards"}`
HTTP/1.1 204 No Content
--data-binary '{"to":["info@egroupware.org"],"subject":"Testmail","body":"This is a test :)\n\nRegards"}'
HTTP/1.1 200 Ok
Content-Type: application/json
{
"status": 200,
"message": "Mail successful sent"
}
```
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
Content-Type: application/json
Content-Length: ...
{"error": 123,"message":"SMTP Server not reachable"}
{"error": 500,"message":"SMTP Server not reachable"}
```
</details>
@ -79,21 +85,7 @@ Content-Type: application/json
{
"status": 200,
"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"
}
}
"message": "Request to open compose window sent"
}
```
- 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.
```
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>'
HTTP/1.1 204 No Content
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);
}
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
if ($name=='mimeType' && !empty($_REQUEST['preset'][$name]))
@ -2504,7 +2504,7 @@ class mail_compose
$disableRuler = false;
$signature = $_identity['ident_signature'];
$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
// 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 {
if (str_starts_with($path, '/mail/attachments/'))
{
$attachment_path = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'attach--'.
(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');
return self::storeAttachment($path, $options['content']);
}
elseif (preg_match('#^/mail/((\d+):(\d+)/)?(compose)?#', $path, $matches))
elseif (preg_match('#^/mail(/(\d+))?(/compose)?#', $path, $matches))
{
$acc_id = $matches[2] ?? null;
$ident_id = $matches[3] ?? null;
$do_compose = (bool)($matches[4] ?? false);
$ident_id = $matches[2] ?? self::defaultIdentity($user);
$do_compose = (bool)($matches[3] ?? false);
if (!($data = json_decode($options['content'], true)))
{
throw new \Exception('Error decoding JSON: '.json_last_error_msg(), 422);
}
// 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)
if ($do_compose)
{
@ -91,39 +84,38 @@ class ApiHandler extends Api\CalDAV\Handler
$account_lid = Api\Accounts::id2name($user);
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->call('egw.open_link', $link, '_blank', '640x1024');
$push->call('egw.open', '', 'mail', 'add', $extra, '_blank', 'mail');
$push->call('egw.open', '', 'mail', 'add', ['preset' => $preset], '_blank', 'mail');
header('Content-Type: application/json');
echo json_encode([
'status' => 200,
'message' => 'Request to open compose window sent',
'extra' => $extra,
//'data' => $preset,
], self::JSON_RESPONSE_OPTIONS);
return true;
}
$compose = new mail_compose($acc_id);
$compose->compose([
'mailaccount' => $acc_id.':'.$ident_id,
'mail_plaintext' => $data['body'] ?? null,
'mail_htmltext' => $data['bodyHtml'] ?? null,
'mimeType' => !empty($data['bodyHtml']) ? 'html' : 'plain',
'file' => array_map(__CLASS__.'::uploadsForAttachments', $data['attachments'] ?? []),
]+array_diff_key($data+array_flip(['attachments', 'body', 'bodyHtml'])));
$compose = new \mail_compose($acc_id=Api\Mail\Account::read_identity($ident_id)['acc_id']);
$preset = array_filter([
'mailaccount' => $acc_id,
'mailidentity' => $ident_id,
'identity' => null,
'add_signature' => true, // add signature in send, independent what preference says
]+$preset);
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');
echo $options['content'];
return true;
throw new \Exception('Not Found', 404);
}
catch (\Throwable $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
*
* @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 bool $compose true: for compose window, false: to send
* @return array with values for keys "file", "name" and "filemode"
* @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 = [];
foreach($attachments as $attachment)
@ -161,14 +202,20 @@ class ApiHandler extends Api\CalDAV\Handler
{
throw new \Exception("Attachment $attachment NOT found", 400);
}
$ret['file'][] = $path;
$ret['name'][] = $matches[2];
/*return [
'name' => $matches[2],
'type' => Api\Vfs::mime_content_type($path),
'file' => $path,
'size' => filesize($path),
];*/
if ($compose)
{
$ret['file'][] = $path;
$ret['name'][] = $matches[2];
}
else
{
$ret['attachments'][] = [
'name' => $matches[2],
'type' => Api\Vfs::mime_content_type($path),
'file' => $path,
'size' => filesize($path),
];
}
}
else
{
@ -176,14 +223,20 @@ class ApiHandler extends Api\CalDAV\Handler
{
throw new \Exception("Attachment $attachment NOT found", 400);
}
$ret['file'][] = Api\Vfs::PREFIX.$attachment;
$ret['name'][] = Api\Vfs::basename($attachment);
/*return [
'name' => Api\Vfs::basename($attachment),
'type' => Api\Vfs::mime_content_type($attachment),
'file' => Api\Vfs::PREFIX.$attachment,
'size' => filesize(Api\Vfs::PREFIX.$attachment),
];*/
if ($compose)
{
$ret['file'][] = Api\Vfs::PREFIX.$attachment;
$ret['name'][] = Api\Vfs::basename($attachment);
}
else
{
$ret['attachments'][] = [
'name' => Api\Vfs::basename($attachment),
'type' => Api\Vfs::mime_content_type($attachment),
'file' => Api\Vfs::PREFIX.$attachment,
'size' => filesize(Api\Vfs::PREFIX.$attachment),
];
}
}
}
if ($ret)