Skip to content

Commit f030092

Browse files
eypsilonclaude
andcommitted
Fix Vercel configuration for PHP serverless functions
- Move get.php to api/ directory (required by Vercel) - Update vercel.json to use api/**/*.php pattern - Add rewrite rule to map /example/get.php to /api/get.php - Fixes "unmatched function pattern" deployment error 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent f5e7153 commit f030092

File tree

2 files changed

+226
-8
lines changed

2 files changed

+226
-8
lines changed

api/get.php

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* IMAP API Endpoint - Powered by ImapService
5+
*/
6+
7+
use Yai\Ymap\ImapService;
8+
use Yai\Ymap\Exceptions\ConnectionException;
9+
10+
error_reporting(E_ALL);
11+
ini_set('display_errors', '0');
12+
13+
header('Content-Type: application/json');
14+
15+
require_once __DIR__ . '/../vendor/autoload.php';
16+
17+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
18+
http_response_code(405);
19+
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
20+
exit;
21+
}
22+
23+
$messageUid = isset($_GET['message']) ? (int) $_GET['message'] : null;
24+
if (null !== $messageUid && $messageUid <= 0) {
25+
$messageUid = null;
26+
}
27+
$messageAction = $_GET['action'] ?? null;
28+
29+
$bodyLength = (int) ($_POST['body_length'] ?? 500);
30+
if ($bodyLength < 100) {
31+
$bodyLength = 100;
32+
} elseif ($bodyLength > 20000) {
33+
$bodyLength = 20000;
34+
}
35+
36+
$username = trim($_POST['username'] ?? '');
37+
$password = trim($_POST['password'] ?? '');
38+
39+
if (!$username || !$password) {
40+
http_response_code(400);
41+
echo json_encode(['success' => false, 'error' => 'Username and password required']);
42+
exit;
43+
}
44+
45+
try {
46+
$imap = ImapService::create()
47+
->connect(
48+
$_POST['mailbox'] ?: '{imap.gmail.com:993/imap/ssl}INBOX',
49+
$username,
50+
$password
51+
)
52+
->fields([
53+
'uid',
54+
'subject',
55+
'from',
56+
'to',
57+
'cc',
58+
'replyTo',
59+
'date',
60+
'textBody',
61+
'htmlBody',
62+
'attachments',
63+
'seen',
64+
'answered',
65+
'preview',
66+
])
67+
->limit((int) ($_POST['limit'] ?? 10))
68+
->orderBy('desc');
69+
70+
// Add optional filters
71+
if (!empty($_POST['date_from'])) {
72+
$imap->since($_POST['date_from']);
73+
}
74+
if (!empty($_POST['date_to'])) {
75+
$imap->before($_POST['date_to']);
76+
}
77+
if (($_POST['read_status'] ?? '') === 'UNREAD') {
78+
$imap->unreadOnly();
79+
} elseif (($_POST['read_status'] ?? '') === 'READ') {
80+
$imap->readOnly();
81+
}
82+
if (($_POST['answered_status'] ?? '') === 'ANSWERED') {
83+
$imap->answeredOnly();
84+
} elseif (($_POST['answered_status'] ?? '') === 'UNANSWERED') {
85+
$imap->unansweredOnly();
86+
}
87+
if (!empty($_POST['search_text'])) {
88+
match ($_POST['search_field'] ?? 'SUBJECT') {
89+
'FROM' => $imap->from($_POST['search_text']),
90+
'TO' => $imap->to($_POST['search_text']),
91+
'BODY', 'TEXT' => $imap->bodyContains($_POST['search_text']),
92+
default => $imap->subjectContains($_POST['search_text']),
93+
};
94+
}
95+
96+
// Exclusion filters from textareas (one pattern per line)
97+
if (!empty($_POST['exclude_from'])) {
98+
$patterns = array_filter(array_map('trim', explode("\n", $_POST['exclude_from'])));
99+
if ($patterns) {
100+
$imap->excludeFrom($patterns);
101+
}
102+
}
103+
if (!empty($_POST['exclude_subject'])) {
104+
$patterns = array_filter(array_map('trim', explode("\n", $_POST['exclude_subject'])));
105+
if ($patterns) {
106+
$imap->excludeSubjectContains($patterns);
107+
}
108+
}
109+
110+
$mapAddresses = static function (array $addresses): array {
111+
$result = [];
112+
113+
foreach ($addresses as $entry) {
114+
if (is_array($entry)) {
115+
$result[] = [
116+
'email' => (string) ($entry['email'] ?? ''),
117+
'name' => $entry['name'] ?? null,
118+
];
119+
continue;
120+
}
121+
122+
$value = (string) $entry;
123+
if (preg_match('/^(.*)<([^>]+)>$/', $value, $matches)) {
124+
$result[] = [
125+
'email' => trim($matches[2]),
126+
'name' => trim($matches[1]) ?: null,
127+
];
128+
} else {
129+
$result[] = [
130+
'email' => $value,
131+
'name' => null,
132+
];
133+
}
134+
}
135+
136+
return $result;
137+
};
138+
139+
$formatMessage = static function (array $msg) use ($bodyLength, $mapAddresses) {
140+
// Use the built-in preview which handles text/HTML fallback and whitespace
141+
$body = $msg['preview'] ?? '';
142+
$bodyPreview = mb_substr($body, 0, $bodyLength);
143+
$isTruncated = mb_strlen($body) > $bodyLength;
144+
145+
return [
146+
'uid' => $msg['uid'],
147+
'subject' => $msg['subject'] ?? '',
148+
'from' => $mapAddresses($msg['from'] ?? []),
149+
'to' => $mapAddresses($msg['to'] ?? []),
150+
'cc' => $mapAddresses($msg['cc'] ?? []),
151+
'replyTo' => $mapAddresses($msg['replyTo'] ?? []),
152+
'date' => $msg['date'] ? date('M j, Y H:i', strtotime($msg['date'])) : null,
153+
'bodyPreview' => $bodyPreview,
154+
'bodyFull' => $body,
155+
'bodyTruncated' => $isTruncated,
156+
'htmlBody' => $msg['htmlBody'] ?? null,
157+
'seen' => (bool) ($msg['seen'] ?? false),
158+
'answered' => (bool) ($msg['answered'] ?? false),
159+
'attachments' => array_map(static fn($a) => [
160+
'filename' => $a['filename'],
161+
'size' => $a['size'],
162+
'sizeFormatted' => number_format($a['size'] / 1024, 1) . ' KB',
163+
], $msg['attachments'] ?? []),
164+
];
165+
};
166+
167+
if (null !== $messageUid) {
168+
$actionMap = [
169+
'mark-read' => 'markAsRead',
170+
'mark-unread' => 'markAsUnread',
171+
'mark-answered' => 'markAsAnswered',
172+
'mark-unanswered' => 'markAsUnanswered',
173+
];
174+
175+
if (null !== $messageAction) {
176+
if (!isset($actionMap[$messageAction])) {
177+
http_response_code(400);
178+
echo json_encode(['success' => false, 'error' => 'Invalid action requested']);
179+
exit;
180+
}
181+
182+
$imap->{$actionMap[$messageAction]}($messageUid);
183+
}
184+
185+
$singleMessage = $imap->getMessage($messageUid);
186+
if (null === $singleMessage) {
187+
http_response_code(404);
188+
echo json_encode(['success' => false, 'error' => 'Message not found']);
189+
exit;
190+
}
191+
192+
echo json_encode([
193+
'success' => true,
194+
'message' => $formatMessage($singleMessage),
195+
], JSON_INVALID_UTF8_SUBSTITUTE);
196+
197+
exit;
198+
}
199+
200+
$messages = $imap->getMessages();
201+
202+
// Format for frontend
203+
$output = array_map($formatMessage, $messages);
204+
205+
// Get total count matching criteria (without limit)
206+
$totalFound = $imap->getTotalCount($imap->getConfig()->toImapCriteria());
207+
208+
echo json_encode([
209+
'success' => true,
210+
'count' => count($output),
211+
'totalFound' => $totalFound,
212+
'searchCriteria' => $imap->getConfig()->toImapCriteria(),
213+
'messages' => $output,
214+
], JSON_INVALID_UTF8_SUBSTITUTE);
215+
216+
} catch (ConnectionException $e) {
217+
http_response_code(401);
218+
echo json_encode(['success' => false, 'error' => 'Connection failed: ' . $e->getMessage()]);
219+
} catch (\Throwable $e) {
220+
http_response_code(500);
221+
echo json_encode(['success' => false, 'error' => 'Error: ' . $e->getMessage()]);
222+
}

vercel.json

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
{
22
"functions": {
3-
"example/get.php": {
3+
"api/**/*.php": {
44
"runtime": "vercel-php@0.7.1"
55
}
66
},
7-
"routes": [
7+
"rewrites": [
88
{
9-
"src": "/example",
10-
"dest": "/example/index.html"
11-
},
12-
{
13-
"src": "/example/(.*)",
14-
"dest": "/example/$1"
9+
"source": "/example/get.php",
10+
"destination": "/api/get.php"
1511
}
1612
]
1713
}

0 commit comments

Comments
 (0)