Skip to content

Commit 81a9c01

Browse files
committed
Implement SieveProviderAdapter and specific provider adapters for enhanced Sieve script generation and management. Add CLI script for testing Sieve connections with user and server configurations (not fully implemete yet)
1 parent 7ca5ae3 commit 81a9c01

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed

lib/SieveProviderAdapter.php

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
<?php
2+
3+
/**
4+
* Sieve Provider Adapter
5+
* Provides provider-aware helpers for Sieve script generation.
6+
*
7+
* @package framework
8+
* @subpackage spam_filtering
9+
*/
10+
11+
if (!defined('DEBUG_MODE')) { die(); }
12+
13+
/**
14+
* Sieve Provider Adapter
15+
*/
16+
class SieveProviderAdapter {
17+
18+
/**
19+
* Adapter context
20+
* @var array
21+
*/
22+
protected $context = array();
23+
protected $overrides = array();
24+
25+
/**
26+
* Supported provider specific adapter classes
27+
* @var array
28+
*/
29+
protected static $provider_map = array(
30+
'kolabnow' => SieveProviderKolabAdapter::class,
31+
'kolabnow.com' => SieveProviderKolabAdapter::class,
32+
'kolab' => SieveProviderKolabAdapter::class,
33+
'migadu' => SieveProviderMigaduAdapter::class,
34+
'migadu.com' => SieveProviderMigaduAdapter::class,
35+
'gmail' => SieveProviderGmailAdapter::class,
36+
'gmail.com' => SieveProviderGmailAdapter::class,
37+
'fastmail' => SieveProviderFastmailAdapter::class,
38+
'fastmail.com' => SieveProviderFastmailAdapter::class,
39+
'gandi' => SieveProviderGandiAdapter::class,
40+
'gmx' => SieveProviderGmxAdapter::class,
41+
'mail.com' => SieveProviderMailComAdapter::class,
42+
'mailbox.org' => SieveProviderMailboxAdapter::class,
43+
'office365' => SieveProviderOffice365Adapter::class,
44+
'outlook.com' => SieveProviderOffice365Adapter::class,
45+
'hotmail' => SieveProviderOffice365Adapter::class,
46+
'live.com' => SieveProviderOffice365Adapter::class,
47+
'postale' => SieveProviderPostaleAdapter::class,
48+
'postale.io' => SieveProviderPostaleAdapter::class,
49+
'yahoo' => SieveProviderYahooAdapter::class,
50+
'yahoo.com' => SieveProviderYahooAdapter::class,
51+
'yandex' => SieveProviderYandexAdapter::class,
52+
'zoho' => SieveProviderZohoAdapter::class,
53+
'aol' => SieveProviderAolAdapter::class,
54+
'aol.com' => SieveProviderAolAdapter::class,
55+
'all-inkl' => SieveProviderAllInklAdapter::class,
56+
'kasserver' => SieveProviderAllInklAdapter::class,
57+
'icloud' => SieveProviderIcloudAdapter::class,
58+
'mail.me.com' => SieveProviderIcloudAdapter::class,
59+
'inbox.com' => SieveProviderInboxAdapter::class
60+
);
61+
62+
/**
63+
* Factory method
64+
* @param array $context
65+
* @return SieveProviderAdapter
66+
*/
67+
public static function create(array $context) {
68+
$tokens = array();
69+
if (!empty($context['provider'])) {
70+
$tokens[] = strtolower($context['provider']);
71+
}
72+
if (!empty($context['host'])) {
73+
$tokens[] = strtolower($context['host']);
74+
}
75+
if (!empty($context['server_name'])) {
76+
$tokens[] = strtolower($context['server_name']);
77+
}
78+
if (!empty($context['server_config']['server'])) {
79+
$tokens[] = strtolower($context['server_config']['server']);
80+
}
81+
82+
foreach ($tokens as $token) {
83+
foreach (self::$provider_map as $key => $class) {
84+
if (strpos($token, $key) !== false && class_exists($class)) {
85+
return new $class($context);
86+
}
87+
}
88+
}
89+
90+
return new self($context);
91+
}
92+
93+
/**
94+
* Constructor
95+
* @param array $context
96+
*/
97+
public function __construct(array $context) {
98+
$this->context = $context;
99+
$this->overrides = isset($context['overrides']) && is_array($context['overrides'])
100+
? $context['overrides']
101+
: array();
102+
}
103+
104+
/**
105+
* Resolve target junk folder
106+
* @param string|null $custom_folder
107+
* @return string
108+
*/
109+
public function resolveJunkFolder($custom_folder = null) {
110+
if (!empty($custom_folder)) {
111+
return $custom_folder;
112+
}
113+
114+
if (!empty($this->overrides['junk_folder'])) {
115+
return $this->overrides['junk_folder'];
116+
}
117+
118+
$defaults = $this->context['defaults'] ?? array();
119+
if (!empty($defaults['junk_folder'])) {
120+
return $defaults['junk_folder'];
121+
}
122+
123+
$special = $this->context['special_folders'] ?? array();
124+
if (!empty($special['junk'])) {
125+
return $special['junk'];
126+
}
127+
if (!empty($special['spam'])) {
128+
return $special['spam'];
129+
}
130+
131+
return 'Junk';
132+
}
133+
134+
/**
135+
* Get required extensions filtered by capabilities
136+
* @param array $extra
137+
* @return array
138+
*/
139+
public function getRequiredExtensions(array $extra = array()) {
140+
$required = array_merge(
141+
array('fileinto'),
142+
(array)($this->context['defaults']['require'] ?? array()),
143+
(array)($this->overrides['require'] ?? array()),
144+
$extra
145+
);
146+
147+
$extensions = $this->getSupportedExtensions();
148+
149+
if (empty($extensions)) {
150+
return array_values(array_unique(array_filter($required)));
151+
}
152+
153+
$filtered = array();
154+
foreach ($required as $ext) {
155+
$ext_lc = strtolower($ext);
156+
if (in_array($ext_lc, $extensions, true)) {
157+
$filtered[] = $ext_lc;
158+
}
159+
}
160+
161+
return array_values(array_unique($filtered));
162+
}
163+
164+
/**
165+
* Determine if provider supports replacing scripts
166+
* @return bool
167+
*/
168+
public function supportsReplaceScript() {
169+
if (isset($this->overrides['supports_replace'])) {
170+
return (bool)$this->overrides['supports_replace'];
171+
}
172+
173+
return !empty($this->context['defaults']['supports_replace']);
174+
}
175+
176+
/**
177+
* Sanitize folder name for Sieve
178+
* @param string $folder
179+
* @return string
180+
*/
181+
public function sanitizeFolderName($folder) {
182+
if ($folder === '') {
183+
return '"Junk"';
184+
}
185+
186+
$needs_quotes = preg_match('/[^A-Za-z0-9._-]/', $folder);
187+
$escaped = str_replace(array('\\', '"'), array('\\\\', '\\"'), $folder);
188+
189+
if ($needs_quotes) {
190+
return '"' . $escaped . '"';
191+
}
192+
193+
return '"' . $escaped . '"';
194+
}
195+
196+
/**
197+
* Get supported extensions from capabilities
198+
* @return array
199+
*/
200+
protected function getSupportedExtensions() {
201+
$capabilities = $this->context['capabilities'] ?? array();
202+
203+
if (isset($capabilities['extensions']) && is_array($capabilities['extensions'])) {
204+
return array_map('strtolower', $capabilities['extensions']);
205+
}
206+
207+
if (isset($capabilities['SIEVE'])) {
208+
$extensions = preg_split('/\s+/', strtolower($capabilities['SIEVE']));
209+
return array_filter($extensions, 'strlen');
210+
}
211+
212+
if (isset($capabilities['sieve'])) {
213+
$extensions = preg_split('/\s+/', strtolower($capabilities['sieve']));
214+
return array_filter($extensions, 'strlen');
215+
}
216+
217+
return array();
218+
}
219+
}
220+
221+
/**
222+
* Kolab specific adapter
223+
*/
224+
class SieveProviderKolabAdapter extends SieveProviderAdapter {
225+
public function supportsReplaceScript() {
226+
return true;
227+
}
228+
}
229+
230+
/**
231+
* Migadu specific adapter
232+
*/
233+
class SieveProviderMigaduAdapter extends SieveProviderAdapter {
234+
}
235+
236+
/**
237+
* Gmail specific adapter
238+
*/
239+
class SieveProviderGmailAdapter extends SieveProviderAdapter {
240+
public function resolveJunkFolder($custom_folder = null) {
241+
if (!empty($custom_folder)) {
242+
return $custom_folder;
243+
}
244+
return '[Gmail]/Spam';
245+
}
246+
}
247+
248+
class SieveProviderFastmailAdapter extends SieveProviderAdapter {}
249+
class SieveProviderGandiAdapter extends SieveProviderAdapter {}
250+
class SieveProviderGmxAdapter extends SieveProviderAdapter {}
251+
class SieveProviderMailComAdapter extends SieveProviderAdapter {}
252+
class SieveProviderMailboxAdapter extends SieveProviderAdapter {}
253+
254+
class SieveProviderOffice365Adapter extends SieveProviderAdapter {
255+
public function resolveJunkFolder($custom_folder = null) {
256+
if (!empty($custom_folder)) {
257+
return $custom_folder;
258+
}
259+
return 'Junk Email';
260+
}
261+
}
262+
263+
class SieveProviderPostaleAdapter extends SieveProviderAdapter {}
264+
class SieveProviderYahooAdapter extends SieveProviderAdapter {}
265+
class SieveProviderYandexAdapter extends SieveProviderAdapter {}
266+
class SieveProviderZohoAdapter extends SieveProviderAdapter {}
267+
class SieveProviderAolAdapter extends SieveProviderAdapter {}
268+
class SieveProviderAllInklAdapter extends SieveProviderAdapter {}
269+
class SieveProviderIcloudAdapter extends SieveProviderAdapter {
270+
public function resolveJunkFolder($custom_folder = null) {
271+
if (!empty($custom_folder)) {
272+
return $custom_folder;
273+
}
274+
return 'Junk';
275+
}
276+
}
277+
class SieveProviderInboxAdapter extends SieveProviderAdapter {}
278+

scripts/sieve-test-connection.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
define('APP_PATH', dirname(__DIR__) . '/');
5+
define('VENDOR_PATH', APP_PATH . 'vendor/');
6+
define('DEBUG_MODE', true);
7+
8+
require VENDOR_PATH . 'autoload.php';
9+
require APP_PATH . 'lib/framework.php';
10+
require APP_PATH . 'modules/imap/hm-imap.php';
11+
12+
$site_config = new Hm_Site_Config_File();
13+
$user_config = new Hm_User_Config_File($site_config);
14+
15+
class SieveCliSession {
16+
public function get($key, $default = null) {
17+
return $default;
18+
}
19+
public function set($key, $value) {
20+
return null;
21+
}
22+
}
23+
24+
$session = new SieveCliSession();
25+
26+
if ($argc < 3) {
27+
fwrite(STDERR, "Usage: php scripts/sieve-test-connection.php <user_id> <imap_server_id>\n");
28+
exit(1);
29+
}
30+
31+
$user_id = $argv[1];
32+
$server_id = $argv[2];
33+
34+
$user_config->load($user_id, null);
35+
36+
$result = SieveSync::testConnection($user_id, $server_id, $user_config, $session);
37+
38+
if ($result['success']) {
39+
echo "✅ Connection successful\n";
40+
if (!empty($result['capabilities'])) {
41+
echo "Capabilities:\n";
42+
foreach ($result['capabilities'] as $key => $value) {
43+
if (is_array($value)) {
44+
echo " {$key}: " . implode(', ', $value) . "\n";
45+
} else {
46+
echo " {$key}: {$value}\n";
47+
}
48+
}
49+
}
50+
exit(0);
51+
}
52+
53+
echo "❌ Connection failed: {$result['message']}\n";
54+
if (!empty($result['details']['suggestion'])) {
55+
echo "Suggestion: {$result['details']['suggestion']}\n";
56+
}
57+
exit(1);
58+

0 commit comments

Comments
 (0)