<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\models;
use seekquarry\yioop\configs as C;
/**
* Persists mail aliases: alternate addresses that deliver to a
* Yioop user's local mailbox and that the user (or an admin) can
* pick as a From address when composing. An alias is a local-part
* paired with one configured mail domain, so an alias row for
* "chris" at "example.com" owned by user "cpollett" makes
* chris@example.com deliver to cpollett, while chris at another
* domain is a separate alias that may be unclaimed or owned by
* someone else.
*
* An alias shares the username namespace: its local-part must not
* collide with an existing Yioop account name, and no two users
* may claim the same alias address. The MAIL_ALIAS table's unique
* index on (ALIAS, DOMAIN) enforces the address-uniqueness half at
* the database level; aliasInUse adds the account-name half and is
* the check call sites should make before adding.
*/
class MailAliasModel extends Model
{
/**
* Reports whether a proposed alias name is already claimed,
* either by an existing Yioop account name or by an existing
* alias (any user's). Comparison is case-insensitive, matching
* how account names and inbound recipient local-parts are
* compared elsewhere. The optional $ignore_user_id excludes a
* user's own aliases from the alias half of the check so a
* re-add of an alias the user already owns is not reported as
* a conflict; account-name collisions are always reported.
*
* @param string $name proposed alias local-part
* @param string $domain the mail domain the alias is for
* @param int $ignore_user_id user whose own aliases to ignore,
* or 0 to consider every alias
* @return bool true when the name is already in use
*/
public function aliasInUse($name, $domain, $ignore_user_id = 0)
{
$db = $this->db;
$name = trim((string) $name);
$domain = strtolower(trim((string) $domain));
if ($name === "" || $domain === "") {
return true;
}
$sql = "SELECT USER_ID FROM USERS " .
"WHERE LOWER(USER_NAME) = LOWER(?) " .
$db->limitOffset(1);
$result = $db->execute($sql, [$name]);
if ($result && $db->fetchArray($result)) {
return true;
}
$sql = "SELECT ID FROM MAIL_ALIAS " .
"WHERE LOWER(ALIAS) = LOWER(?) AND LOWER(DOMAIN) = ?";
$parameters = [$name, $domain];
if ($ignore_user_id > 0) {
$sql .= " AND USER_ID <> ?";
$parameters[] = (int) $ignore_user_id;
}
$sql .= " " . $db->limitOffset(1);
$result = $db->execute($sql, $parameters);
if ($result && $db->fetchArray($result)) {
return true;
}
return false;
}
/**
* Adds an alias for a user after confirming the name is not
* already claimed by an account or another user's alias. A
* no-op returning false when the name is unavailable, so a
* caller can branch on the result to surface a message.
*
* @param int $user_id owning Yioop user id
* @param string $alias the alias local-part to add
* @param string $domain the mail domain the alias is for
* @return bool true when the alias was added
*/
public function addAlias($user_id, $alias, $domain)
{
$db = $this->db;
$alias = trim((string) $alias);
$domain = strtolower(trim((string) $domain));
if ($alias === "" || $domain === "" ||
$this->aliasInUse($alias, $domain)) {
return false;
}
$sql = "INSERT INTO MAIL_ALIAS " .
"(USER_ID, ALIAS, DOMAIN, CREATED_AT) " .
"VALUES (?, ?, ?, ?)";
$db->execute($sql,
[(int) $user_id, $alias, $domain, time()]);
return true;
}
/**
* Removes one alias owned by a user. Scoped to the owning user
* so a request cannot delete another user's alias. Matching is
* case-insensitive on the alias name.
*
* @param int $user_id owning Yioop user id
* @param string $alias the alias local-part to remove
* @param string $domain the mail domain of the alias to remove
*/
public function removeAlias($user_id, $alias, $domain)
{
$db = $this->db;
$sql = "DELETE FROM MAIL_ALIAS " .
"WHERE USER_ID = ? AND LOWER(ALIAS) = LOWER(?) " .
"AND LOWER(DOMAIN) = ?";
$db->execute($sql, [(int) $user_id,
trim((string) $alias),
strtolower(trim((string) $domain))]);
}
/**
* Returns the aliases a user owns as alias/domain pairs, sorted
* for a stable display order. Each entry is an associative
* array with 'ALIAS' and 'DOMAIN' keys, together forming the
* full alias address.
*
* @param int $user_id owning Yioop user id
* @return array list of ['ALIAS' => ..., 'DOMAIN' => ...]
*/
public function aliasesForUser($user_id)
{
$db = $this->db;
$sql = "SELECT ALIAS, DOMAIN FROM MAIL_ALIAS " .
"WHERE USER_ID = ? ORDER BY ALIAS ASC, DOMAIN ASC";
$result = $db->execute($sql, [(int) $user_id]);
$aliases = [];
if ($result) {
while ($row = $db->fetchArray($result)) {
$aliases[] = ['ALIAS' => $row['ALIAS'],
'DOMAIN' => $row['DOMAIN']];
}
}
return $aliases;
}
/**
* Lists the addresses a user may send mail as: their primary
* address followed by every alias they own (each written as
* alias@domain). Used to populate the "From" choices on the
* compose form.
*
* @param int $user_id the user whose identities are wanted
* @param string $primary the user's primary sending address,
* placed first
* @return array the primary address followed by the alias
* addresses
*/
public function identitiesFor($user_id, $primary)
{
$identities = [$primary];
foreach ($this->aliasesForUser($user_id) as $entry) {
$identities[] = $entry["ALIAS"] . '@' . $entry["DOMAIN"];
}
return $identities;
}
/**
* Returns the owning user id for an alias address, or 0 when no
* such alias is registered. Used by inbound delivery to route a
* recipient whose local-part is an alias to the right mailbox.
* Both the local-part and the domain must match, so an alias
* resolves only at the domain it was created for. Matching is
* case-insensitive.
*
* @param string $alias the alias local-part to resolve
* @param string $domain the recipient domain to resolve at
* @return int owning user id, or 0 when not found
*/
public function userIdForAlias($alias, $domain)
{
$db = $this->db;
$sql = "SELECT USER_ID FROM MAIL_ALIAS " .
"WHERE LOWER(ALIAS) = LOWER(?) AND LOWER(DOMAIN) = ? " .
$db->limitOffset(1);
$result = $db->execute($sql, [trim((string) $alias),
strtolower(trim((string) $domain))]);
if ($result) {
$row = $db->fetchArray($result);
if ($row && isset($row['USER_ID'])) {
return (int) $row['USER_ID'];
}
}
return 0;
}
}