mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-01-23 02:14:26 +00:00
Merge 299a81502a into e727620bd3
This commit is contained in:
commit
00ebb3bcca
4 changed files with 553 additions and 0 deletions
|
|
@ -6047,6 +6047,164 @@ paths:
|
|||
ignore_ssl_error: true
|
||||
summary: Edit external Identity Provider
|
||||
|
||||
/api/v1/send/email:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- smtp_api
|
||||
- send
|
||||
- from: sender@domain.tld
|
||||
to:
|
||||
- recipient@domain.tld
|
||||
subject: Test Subject
|
||||
body: "***"
|
||||
password: "*"
|
||||
msg:
|
||||
- smtp_mail_sent
|
||||
- sender@domain.tld
|
||||
- recipient@domain.tld
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
tags:
|
||||
- SMTP
|
||||
description: >-
|
||||
Send an email via SMTP using the mailcow SMTP API. This endpoint allows
|
||||
you to send emails programmatically with support for HTML content,
|
||||
attachments, CC, BCC, and custom SMTP settings.
|
||||
|
||||
|
||||
**Authentication**: Requires a read-write API key.
|
||||
|
||||
|
||||
**Note**: SMTP authentication with mailbox credentials is required.
|
||||
Use port 587 for STARTTLS or port 465 for SSL/TLS.
|
||||
operationId: Send email
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
from: sender@domain.tld
|
||||
to:
|
||||
- recipient@domain.tld
|
||||
subject: Test Subject
|
||||
body: This is the plain text body
|
||||
html_body: "<html><body><h1>Hello</h1><p>This is HTML content</p></body></html>"
|
||||
cc:
|
||||
- cc@domain.tld
|
||||
bcc:
|
||||
- bcc@domain.tld
|
||||
reply_to: reply@domain.tld
|
||||
attachments:
|
||||
- filename: document.pdf
|
||||
content: base64encodedcontent
|
||||
content_type: application/pdf
|
||||
smtp_host: postfix-mailcow
|
||||
smtp_port: 587
|
||||
smtp_user: sender@domain.tld
|
||||
password: yourpassword
|
||||
properties:
|
||||
from:
|
||||
description: Sender email address
|
||||
type: string
|
||||
to:
|
||||
description: Array of recipient email addresses
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
subject:
|
||||
description: Email subject
|
||||
type: string
|
||||
body:
|
||||
description: Plain text email body
|
||||
type: string
|
||||
html_body:
|
||||
description: HTML email body (optional)
|
||||
type: string
|
||||
cc:
|
||||
description: Array of CC email addresses (optional)
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
bcc:
|
||||
description: Array of BCC email addresses (optional)
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
reply_to:
|
||||
description: Reply-to email address (optional)
|
||||
type: string
|
||||
attachments:
|
||||
description: Array of attachments with base64 encoded content (optional)
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
filename:
|
||||
type: string
|
||||
description: Attachment filename
|
||||
content:
|
||||
type: string
|
||||
description: Base64 encoded file content
|
||||
content_type:
|
||||
type: string
|
||||
description: MIME content type
|
||||
smtp_host:
|
||||
description: SMTP server host (default postfix-mailcow)
|
||||
type: string
|
||||
smtp_port:
|
||||
description: SMTP server port (default 587 for authenticated; set explicitly if different)
|
||||
type: integer
|
||||
smtp_user:
|
||||
description: SMTP username (mailbox login; required)
|
||||
type: string
|
||||
password:
|
||||
description: >
|
||||
SMTP password (mailbox password or mailbox app password; required).
|
||||
App passwords are recommended for API usage.
|
||||
type: string
|
||||
example: "app-password-or-mailbox-password"
|
||||
app_password:
|
||||
description: >
|
||||
Alias for `password`.
|
||||
SMTP password (mailbox password or mailbox app password; required).
|
||||
App passwords are recommended for API usage.
|
||||
type: string
|
||||
example: "app-password-or-mailbox-password"
|
||||
required:
|
||||
- from
|
||||
- to
|
||||
- subject
|
||||
- body
|
||||
- smtp_user
|
||||
- password
|
||||
type: object
|
||||
summary: Send email via SMTP
|
||||
|
||||
tags:
|
||||
- name: Domains
|
||||
description: You can create antispam whitelist and blacklist policies
|
||||
|
|
@ -6094,3 +6252,5 @@ tags:
|
|||
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||
- name: Identity Provider
|
||||
description: Manage external Identity Provider settings
|
||||
- name: SMTP
|
||||
description: Send emails programmatically via SMTP
|
||||
|
|
|
|||
|
|
@ -3387,6 +3387,331 @@ function reset_password($action, $data = null) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
function smtp_api($action, $data) {
|
||||
global $pdo;
|
||||
$_data_log = $data;
|
||||
// Mask password in logs
|
||||
if (isset($_data_log['password'])) {
|
||||
$_data_log['password'] = '*';
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'send':
|
||||
// Validate required fields
|
||||
$from = isset($data['from']) ? $data['from'] : '';
|
||||
$to = isset($data['to']) ? $data['to'] : array();
|
||||
$subject = isset($data['subject']) ? $data['subject'] : '';
|
||||
$body = isset($data['body']) ? $data['body'] : '';
|
||||
|
||||
// Optional fields
|
||||
$html_body = isset($data['html_body']) ? $data['html_body'] : null;
|
||||
$cc = isset($data['cc']) ? $data['cc'] : array();
|
||||
$bcc = isset($data['bcc']) ? $data['bcc'] : array();
|
||||
$reply_to = isset($data['reply_to']) ? $data['reply_to'] : null;
|
||||
$attachments = isset($data['attachments']) ? $data['attachments'] : array();
|
||||
|
||||
// SMTP settings - mailbox-level auth; default to 587 (STARTTLS)
|
||||
$smtp_host = isset($data['smtp_host']) ? $data['smtp_host'] : 'postfix-mailcow';
|
||||
$smtp_user = isset($data['smtp_user']) ? $data['smtp_user'] : '';
|
||||
$smtp_pass = $data['app_password'] ?? $data['password'] ?? '';
|
||||
$smtp_port = isset($data['smtp_port']) ? intval($data['smtp_port']) : 587;
|
||||
|
||||
// Validate from address
|
||||
if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => 'smtp_invalid_from'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Require SMTP auth with mailbox credentials
|
||||
if (empty($smtp_user) || empty($smtp_pass)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => 'smtp_auth_required'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check sender authorization (mailbox-level)
|
||||
$username = strtolower(trim($smtp_user));
|
||||
$from_l = strtolower(trim($from));
|
||||
$sender_authorized = false;
|
||||
|
||||
// 1) Sender matches mailbox user
|
||||
if ($from_l === $username) {
|
||||
$sender_authorized = true;
|
||||
}
|
||||
|
||||
// 2) Alias points to this mailbox
|
||||
if (!$sender_authorized) {
|
||||
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` = :from_address");
|
||||
$stmt->execute(array(
|
||||
':goto' => '(^|,)' . preg_quote($username, '/') . '($|,)',
|
||||
':from_address' => $from
|
||||
));
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$sender_authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) sender_acl specific address
|
||||
if (!$sender_authorized) {
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` = :from_address");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':from_address' => $from
|
||||
));
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$sender_authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) sender_acl domain wildcard
|
||||
if (!$sender_authorized) {
|
||||
$from_domain = substr(strrchr($from, '@'), 1);
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` = :domain_wildcard");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':domain_wildcard' => '@' . $from_domain
|
||||
));
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$sender_authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 5) sender_acl global wildcard
|
||||
if (!$sender_authorized) {
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` = '*'");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$sender_authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 6) External sender aliases
|
||||
if (!$sender_authorized) {
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `external` = '1' AND `send_as` = :from_address");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':from_address' => $from
|
||||
));
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$sender_authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$sender_authorized) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => array('smtp_unauthorized_sender', htmlspecialchars($from))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate to addresses
|
||||
if (empty($to)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => 'smtp_missing_recipients'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure to is an array
|
||||
if (!is_array($to)) {
|
||||
$to = array($to);
|
||||
}
|
||||
|
||||
foreach ($to as $recipient) {
|
||||
if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => array('smtp_invalid_recipient', htmlspecialchars($recipient))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate subject
|
||||
if (empty($subject)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => 'smtp_empty_subject'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate body
|
||||
if (empty($body) && empty($html_body)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => 'smtp_empty_body'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate CC addresses if provided
|
||||
if (!empty($cc)) {
|
||||
if (!is_array($cc)) {
|
||||
$cc = array($cc);
|
||||
}
|
||||
foreach ($cc as $cc_addr) {
|
||||
if (!filter_var($cc_addr, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => array('smtp_invalid_cc', htmlspecialchars($cc_addr))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate BCC addresses if provided
|
||||
if (!empty($bcc)) {
|
||||
if (!is_array($bcc)) {
|
||||
$bcc = array($bcc);
|
||||
}
|
||||
foreach ($bcc as $bcc_addr) {
|
||||
if (!filter_var($bcc_addr, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => array('smtp_invalid_bcc', htmlspecialchars($bcc_addr))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate reply-to if provided
|
||||
if (!empty($reply_to) && !filter_var($reply_to, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => 'smtp_invalid_reply_to'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ini_set('max_execution_time', 0);
|
||||
ini_set('max_input_time', 0);
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->Timeout = 30;
|
||||
$mail->SMTPOptions = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
)
|
||||
);
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $smtp_host;
|
||||
$mail->Port = $smtp_port;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->XMailer = 'mailcow SMTP API';
|
||||
|
||||
// Authentication is required
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $smtp_user;
|
||||
$mail->Password = $smtp_pass;
|
||||
if ($smtp_port == 465) {
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||
} elseif ($smtp_port == 587) {
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||
}
|
||||
|
||||
// Set sender
|
||||
$mail->setFrom($from);
|
||||
|
||||
// Add recipients
|
||||
foreach ($to as $recipient) {
|
||||
$mail->addAddress($recipient);
|
||||
}
|
||||
|
||||
// Add CC
|
||||
if (!empty($cc)) {
|
||||
foreach ($cc as $cc_addr) {
|
||||
$mail->addCC($cc_addr);
|
||||
}
|
||||
}
|
||||
|
||||
// Add BCC
|
||||
if (!empty($bcc)) {
|
||||
foreach ($bcc as $bcc_addr) {
|
||||
$mail->addBCC($bcc_addr);
|
||||
}
|
||||
}
|
||||
|
||||
// Set reply-to
|
||||
if (!empty($reply_to)) {
|
||||
$mail->addReplyTo($reply_to);
|
||||
}
|
||||
|
||||
// Set subject
|
||||
$mail->Subject = $subject;
|
||||
|
||||
// Set body
|
||||
if (!empty($html_body)) {
|
||||
$mail->isHTML(true);
|
||||
$mail->Body = $html_body;
|
||||
$mail->AltBody = $body;
|
||||
} else {
|
||||
$mail->Body = $body;
|
||||
}
|
||||
|
||||
// Add attachments
|
||||
if (!empty($attachments) && is_array($attachments)) {
|
||||
foreach ($attachments as $attachment) {
|
||||
if (isset($attachment['content']) && isset($attachment['filename'])) {
|
||||
$content = base64_decode($attachment['content']);
|
||||
if ($content === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => array('smtp_invalid_attachment', htmlspecialchars($attachment['filename']))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$content_type = isset($attachment['content_type']) ? $attachment['content_type'] : 'application/octet-stream';
|
||||
$mail->addStringAttachment($content, $attachment['filename'], 'base64', $content_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send
|
||||
$mail->send();
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => array('smtp_mail_sent', htmlspecialchars($from), implode(', ', $to))
|
||||
);
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'error',
|
||||
'log' => array(__FUNCTION__, $action, $_data_log),
|
||||
'msg' => array('smtp_error', htmlspecialchars($mail->ErrorInfo))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function clear_session(){
|
||||
session_regenerate_id(true);
|
||||
session_unset();
|
||||
|
|
|
|||
|
|
@ -2018,6 +2018,61 @@ if (isset($_GET['query'])) {
|
|||
exit();
|
||||
}
|
||||
break;
|
||||
case "send":
|
||||
if ($_SESSION['mailcow_cc_api_access'] == 'ro' || isset($_SESSION['pending_mailcow_cc_username'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'API read/write access denied'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
function process_send_return($return) {
|
||||
$generic_failure = json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'Failed to send email'
|
||||
));
|
||||
$generic_success = json_encode(array(
|
||||
'type' => 'success',
|
||||
'msg' => 'Email sent successfully'
|
||||
));
|
||||
if ($return === false) {
|
||||
echo isset($_SESSION['return']) ? json_encode($_SESSION['return']) : $generic_failure;
|
||||
}
|
||||
else {
|
||||
echo isset($_SESSION['return']) ? json_encode($_SESSION['return']) : $generic_success;
|
||||
}
|
||||
}
|
||||
if (!isset($_POST['attr'])) {
|
||||
echo $request_incomplete;
|
||||
exit;
|
||||
}
|
||||
else {
|
||||
$attr = (array)json_decode($_POST['attr'], true);
|
||||
unset($attr['csrf_token']);
|
||||
}
|
||||
// only allow POST requests
|
||||
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'only POST method is allowed'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
switch ($category) {
|
||||
case "email":
|
||||
process_send_return(smtp_api('send', $attr));
|
||||
break;
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'route not found'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
break;
|
||||
// return no route found if no case is matched
|
||||
default:
|
||||
http_response_code(404);
|
||||
|
|
|
|||
|
|
@ -542,6 +542,18 @@
|
|||
"tls_policy_map_dest_invalid": "Policy destination is invalid",
|
||||
"tls_policy_map_entry_exists": "A TLS policy map entry \"%s\" exists",
|
||||
"tls_policy_map_parameter_invalid": "Policy parameter is invalid",
|
||||
"smtp_invalid_from": "Invalid sender email address",
|
||||
"smtp_auth_required": "SMTP username and password are required",
|
||||
"smtp_unauthorized_sender": "You are not authorized to send as %s",
|
||||
"smtp_missing_recipients": "At least one recipient is required",
|
||||
"smtp_invalid_recipient": "Invalid recipient email address: %s",
|
||||
"smtp_empty_subject": "Email subject cannot be empty",
|
||||
"smtp_empty_body": "Email body cannot be empty",
|
||||
"smtp_invalid_cc": "Invalid CC email address: %s",
|
||||
"smtp_invalid_bcc": "Invalid BCC email address: %s",
|
||||
"smtp_invalid_reply_to": "Invalid reply-to email address",
|
||||
"smtp_invalid_attachment": "Invalid base64 attachment: %s",
|
||||
"smtp_error": "SMTP error: %s",
|
||||
"to_invalid": "Recipient must not be empty",
|
||||
"totp_verification_failed": "TOTP verification failed",
|
||||
"transport_dest_exists": "Transport destination \"%s\" exists",
|
||||
|
|
@ -1198,6 +1210,7 @@
|
|||
"saved_settings": "Saved settings",
|
||||
"settings_map_added": "Added settings map entry",
|
||||
"settings_map_removed": "Removed settings map ID %s",
|
||||
"smtp_mail_sent": "Email sent successfully from %s to %s",
|
||||
"sogo_profile_reset": "SOGo profile for user %s was reset",
|
||||
"template_added": "Added template %s",
|
||||
"template_modified": "Changes to template %s have been saved",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue