lookup->mailbox($login) : null; if ($login === '' && $port === self::SERVICE_PORT) { // Sieve redirects and vacation replies use the address of the mailbox, other mail of mailcow passes $account = $this->lookup->mailbox($this->resolve(self::untagged($sender))); if ($account === null) { return Decision::allow(); } } foreach ($this->sendingPolicies($account, $sender) as $policy) { if (!$this->allows($policy->sendMode, $policy->sendAllow, $policy->sendBlock, $this->isInternalRecipient($recipient, $policy->domain), fn(string $entry): bool => $this->recipientMatches($entry, $recipient))) { return Decision::deny('send', $policy); } } foreach ($this->receivingPolicies($recipient) as $policy) { if (!$this->allows($policy->receiveMode, $policy->receiveAllow, $policy->receiveBlock, $account !== null && $account['domain'] === $policy->domain, fn(string $entry): bool => $this->senderMatches($entry, $sender, $account))) { return Decision::deny('receive', $policy); } } return Decision::allow(); } /** Blocked entries first; then the mode decides */ private function allows(string $mode, array $allow, array $block, bool $internal, callable $matches): bool { foreach ($block as $entry) { if ($matches($entry)) { return false; } } return match ($mode) { 'internal' => $internal || $this->any($allow, $matches), 'list' => $this->any($allow, $matches), default => true, }; } private function any(array $entries, callable $matches): bool { foreach ($entries as $entry) { if ($matches($entry)) { return true; } } return false; } /** @return Policy[] of the logged-in mailbox and of the mailbox it sends as */ private function sendingPolicies(?array $account, string $sender): array { if ($account === null) { return []; } $policies = $this->lookup->policiesForMailbox($account['username']); $sentAs = $this->lookup->mailbox($this->resolve(self::untagged($sender))); if ($sentAs !== null && $sentAs['username'] !== $account['username']) { $policies = array_merge($policies, $this->lookup->policiesForMailbox($sentAs['username'])); } return self::unique($policies); } /** @return Policy[] of the addressed mailbox, group and internal alias */ private function receivingPolicies(string $recipient): array { $address = $this->resolve(self::untagged($recipient)); $policies = []; if ($this->lookup->mailbox($address) !== null) { $policies = $this->lookup->policiesForMailbox($address); } $group = $this->lookup->groupByAddress($address); if ($group !== null) { $policies = array_merge($policies, $this->lookup->policiesForGroup($group)); } $aliasDomain = $this->lookup->internalAlias($address); if ($aliasDomain !== null) { // the `internal` flag of an alias or group: only senders of its domain $internal = new Policy(); $internal->domain = $aliasDomain; $internal->receiveMode = 'internal'; $policies[] = $internal; } return self::unique($policies); } private function isInternalRecipient(string $recipient, string $domain): bool { return $this->resolveDomain(self::domainOf($recipient)) === $domain; } private function recipientMatches(string $entry, string $recipient): bool { $address = $this->resolve(self::untagged($recipient)); if (str_starts_with($entry, 'group:')) { $id = (int)substr($entry, 6); return $this->lookup->groupAddress($id) === $address || ($this->lookup->mailbox($address) !== null && $this->lookup->isGroupMember($id, $address)); } if (str_contains($entry, '@')) { return $this->resolve($entry) === $address; } return $this->resolveDomain(self::domainOf($address)) === $this->resolveDomain($entry); } private function senderMatches(string $entry, string $sender, ?array $account): bool { $address = $this->resolve(self::untagged($sender)); if (str_starts_with($entry, 'group:')) { $id = (int)substr($entry, 6); return $account !== null && ($this->lookup->isGroupMember($id, $account['username']) || $this->lookup->groupAddress($id) === $address); } if (str_contains($entry, '@')) { // a local address only for a logged-in sender (Postfix checked that the login may use it) return $this->lookup->isLocalDomain(self::domainOf($entry)) ? $account !== null && ($account['username'] === $this->resolve($entry) || $address === $this->resolve($entry)) : self::untagged($sender) === $entry; } return $this->lookup->isLocalDomain($entry) ? $account !== null && ($account['domain'] === $this->resolveDomain($entry) || $this->resolveDomain(self::domainOf($address)) === $this->resolveDomain($entry)) : self::domainOf($sender) === $entry; } /** Address in an alias domain as the address in its target domain */ private function resolve(string $address): string { $domain = self::domainOf($address); $target = ($domain !== '') ? $this->lookup->aliasDomainTarget($domain) : null; return ($target === null) ? $address : substr($address, 0, strrpos($address, '@') + 1) . $target; } private function resolveDomain(string $domain): string { return ($domain === '') ? '' : ($this->lookup->aliasDomainTarget($domain) ?? $domain); } /** @param Policy[] $policies @return Policy[] without repeated policies */ private static function unique(array $policies): array { $unique = []; foreach ($policies as $policy) { $unique[$policy->id ?? spl_object_id($policy)] = $policy; } return array_values($unique); } /** Lower case address without angle brackets; '' unless local@domain */ private static function address(string $value): string { $value = strtolower(trim($value, " \t\r\n<>")); return preg_match('/^[^@\s,<>]+@[^@\s,<>]+$/', $value) ? $value : ''; } private static function untagged(string $address): string { $at = strrpos($address, '@'); $pos = ($at === false) ? false : strpos(substr($address, 0, $at), '+'); return ($pos === false || $pos === 0) ? $address : substr($address, 0, $pos) . substr($address, $at); } private static function domainOf(string $address): string { $at = strrpos($address, '@'); return ($at === false) ? '' : substr($address, $at + 1); } }