diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 231441bf6..5eb32390c 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -6124,7 +6124,7 @@ paths: content: base64encodedcontent content_type: application/pdf smtp_host: postfix-mailcow - smtp_port: 25 + smtp_port: 587 smtp_user: sender@domain.tld password: yourpassword properties: @@ -6177,19 +6177,21 @@ paths: description: SMTP server host (default postfix-mailcow) type: string smtp_port: - description: SMTP server port (default 25 for internal, use 587 for authenticated) + description: SMTP server port (default 587 for authenticated; set explicitly if different) type: integer smtp_user: - description: SMTP username (default same as from) + description: SMTP username (mailbox login; required) type: string password: - description: SMTP password for authentication + description: SMTP password (mailbox login; required) type: string required: - from - to - subject - body + - smtp_user + - password type: object summary: Send email via SMTP diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 4bc1426f6..44a483c39 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -3410,11 +3410,11 @@ function smtp_api($action, $data) { $reply_to = isset($data['reply_to']) ? $data['reply_to'] : null; $attachments = isset($data['attachments']) ? $data['attachments'] : array(); - // SMTP settings - default to port 25 for unauthenticated internal sending + // SMTP settings - mailbox-level auth; default to 587 (STARTTLS) $smtp_host = isset($data['smtp_host']) ? $data['smtp_host'] : 'postfix-mailcow'; - $smtp_port = isset($data['smtp_port']) ? intval($data['smtp_port']) : 25; - $smtp_user = isset($data['smtp_user']) ? $data['smtp_user'] : $from; + $smtp_user = isset($data['smtp_user']) ? $data['smtp_user'] : ''; $smtp_pass = isset($data['password']) ? $data['password'] : ''; + $smtp_port = isset($data['smtp_port']) ? intval($data['smtp_port']) : 587; // Validate from address if (!filter_var($from, FILTER_VALIDATE_EMAIL)) { @@ -3426,84 +3426,93 @@ function smtp_api($action, $data) { return false; } - // Check sender authorization (admins can bypass, others must be authorized) - if ($_SESSION['mailcow_cc_role'] != 'admin') { - $username = $_SESSION['mailcow_cc_username']; - $sender_authorized = false; - - // Check if from address is the user's own mailbox - if (strtolower($from) == strtolower($username)) { + // 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; } - - if (!$sender_authorized) { - // Check if from address is in user's aliases (goto contains the username) - $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; } - - if (!$sender_authorized) { - // Check sender_acl for specific address - $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; } - - if (!$sender_authorized) { - // Check sender_acl for domain wildcard (@domain.tld) - $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; } - - if (!$sender_authorized) { - // Check sender_acl for global wildcard (*) - $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; - } - } - - if (!$sender_authorized) { - // Check external sender aliases - $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; + } + + // 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( diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 10bff8748..711deac87 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -543,6 +543,7 @@ "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",