mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 06:30:59 +01:00
WIP REST API for mail: non-interactive direct sending of mails
This commit is contained in:
parent
4616fb03d0
commit
8a3fd670ee
@ -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>
|
||||||
|
@ -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.
|
||||||
|
@ -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+):(\d+)/)?(compose)?#', $path, $matches))
|
elseif (preg_match('#^/mail(/(\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);
|
||||||
}
|
}
|
||||||
$ret['file'][] = $path;
|
if ($compose)
|
||||||
$ret['name'][] = $matches[2];
|
{
|
||||||
/*return [
|
$ret['file'][] = $path;
|
||||||
'name' => $matches[2],
|
$ret['name'][] = $matches[2];
|
||||||
'type' => Api\Vfs::mime_content_type($path),
|
}
|
||||||
'file' => $path,
|
else
|
||||||
'size' => filesize($path),
|
{
|
||||||
];*/
|
$ret['attachments'][] = [
|
||||||
|
'name' => $matches[2],
|
||||||
|
'type' => Api\Vfs::mime_content_type($path),
|
||||||
|
'file' => $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);
|
||||||
}
|
}
|
||||||
$ret['file'][] = Api\Vfs::PREFIX.$attachment;
|
if ($compose)
|
||||||
$ret['name'][] = Api\Vfs::basename($attachment);
|
{
|
||||||
/*return [
|
$ret['file'][] = Api\Vfs::PREFIX.$attachment;
|
||||||
'name' => Api\Vfs::basename($attachment),
|
$ret['name'][] = Api\Vfs::basename($attachment);
|
||||||
'type' => Api\Vfs::mime_content_type($attachment),
|
}
|
||||||
'file' => Api\Vfs::PREFIX.$attachment,
|
else
|
||||||
'size' => filesize(Api\Vfs::PREFIX.$attachment),
|
{
|
||||||
];*/
|
$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)
|
if ($ret)
|
||||||
|
Loading…
Reference in New Issue
Block a user