Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
MailDateFormatService
100.00% covered (success)
100.00%
59 / 59
100.00% covered (success)
100.00%
4 / 4
16
100.00% covered (success)
100.00%
1 / 1
 formatRelativeDate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 formatOlderDate
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 formatFullDate
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 enrichSummary
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Mail\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Mail\Domain\Model\MailMessageSummaryDto;
12use DateTimeImmutable;
13use DateTimeZone;
14use Throwable;
15
16/**
17 * Service for formatting email dates into human-readable relative and localized representations.
18 *
19 * @package App\Modules\Mail\Application\Service
20 */
21final readonly class MailDateFormatService
22{
23    public const string DEFAULT_TIMEZONE = 'Europe/Warsaw';
24
25    /**
26     * Formats timestamp into humanized relative time string in English.
27     *
28     * @param int $timestamp Unix epoch in seconds.
29     * @param int|null $now Reference timestamp (defaults to current time).
30     * @return string Humanized relative time string.
31     */
32    public function formatRelativeDate(int $timestamp, ?int $now = null): string
33    {
34        if ($timestamp <= 0) {
35            return '';
36        }
37
38        $now ??= time();
39        $diff = $now - $timestamp;
40
41        return match (true) {
42            $diff < 60    => 'just now',
43            $diff < 3600  => max(1, (int) floor($diff / 60)) . ' min ago',
44            $diff < 86400 => (int) floor($diff / 3600) === 1
45                ? '1 hour ago'
46                : (int) floor($diff / 3600) . ' hours ago',
47            default       => $this->formatOlderDate($timestamp, $diff, $now),
48        };
49    }
50
51    /**
52     * Formats timestamp older than 24 hours into localized date string.
53     *
54     * @param int $timestamp Unix timestamp.
55     * @param int $diff Seconds elapsed since timestamp.
56     * @param int $now Current reference timestamp.
57     * @return string Formatted older date.
58     */
59    private function formatOlderDate(int $timestamp, int $diff, int $now): string
60    {
61        $days = (int) floor($diff / 86400);
62        if ($days === 1) {
63            return 'yesterday';
64        }
65        if ($days < 7) {
66            return $days . ' days ago';
67        }
68
69        $currentYear = (int) date('Y', $now);
70        $msgYear = (int) date('Y', $timestamp);
71        $result = date('Y-m-d', $timestamp);
72        if ($currentYear === $msgYear) {
73            $msgDay = (int) date('j', $timestamp);
74            $monthShort = date('M', $timestamp);
75            $result = $msgDay . ' ' . $monthShort;
76        }
77
78        return $result;
79    }
80
81    /**
82     * Formats timestamp into precise full date string localized to user timezone.
83     *
84     * @param int $timestamp Unix epoch in seconds.
85     * @param string $timezone IANA timezone identifier.
86     * @return string Formatted full date string.
87     */
88    public function formatFullDate(int $timestamp, string $timezone = self::DEFAULT_TIMEZONE): string
89    {
90        if ($timestamp <= 0) {
91            return '';
92        }
93
94        try {
95            $tz = new DateTimeZone($timezone);
96        } catch (Throwable) {
97            $tz = new DateTimeZone(self::DEFAULT_TIMEZONE);
98        }
99
100        $dt = (new DateTimeImmutable('@' . $timestamp))->setTimezone($tz);
101        $day = (int) $dt->format('j');
102        $monthName = $dt->format('F');
103        $year = $dt->format('Y');
104        $time = $dt->format('H:i:s');
105
106        return sprintf('%d %s %s, %s', $day, $monthName, $year, $time);
107    }
108
109    /**
110     * Enriches summary DTO with relative and user-timezone full date strings.
111     *
112     * @param MailMessageSummaryDto $summary Summary DTO.
113     * @param string $timezone User timezone.
114     * @return MailMessageSummaryDto Enriched DTO copy.
115     */
116    public function enrichSummary(
117        MailMessageSummaryDto $summary,
118        string $timezone = self::DEFAULT_TIMEZONE
119    ): MailMessageSummaryDto {
120        $timestamp = $summary->dateTimestamp > 0 ? $summary->dateTimestamp : (int) strtotime($summary->date);
121        $dateRelative = $this->formatRelativeDate($timestamp);
122        $dateFull = $this->formatFullDate($timestamp, $timezone);
123
124        return new MailMessageSummaryDto(
125            uid: $summary->uid,
126            messageId: $summary->messageId,
127            subject: $summary->subject,
128            fromName: $summary->fromName,
129            fromEmail: $summary->fromEmail,
130            to: $summary->to,
131            cc: $summary->cc,
132            date: $summary->date,
133            dateTimestamp: $timestamp,
134            size: $summary->size,
135            isSeen: $summary->isSeen,
136            isFlagged: $summary->isFlagged,
137            isAnswered: $summary->isAnswered,
138            isDraft: $summary->isDraft,
139            hasAttachments: $summary->hasAttachments,
140            snippet: $summary->snippet,
141            folder: $summary->folder,
142            keywords: $summary->keywords,
143            dateRelative: $dateRelative,
144            dateFull: $dateFull
145        );
146    }
147}