Skip to content

Commit f8de2e3

Browse files
TatevikGrtatevikg1
andauthored
release: dev → main (email pipeline refactor + forwarding, placeholders, tracking, attachments) (#378)
* Feat: message sending-forwarding (#374) * UserPersonalizer in CampaignProcessorMessageHandler * HtmlToText * MessageDataLoader * TextParser * RemotePageFetcher * Use repo methods * Use MessagePrecacheDto * Refactor * Todo * SystemMailConstructor * EmailBuilder * InjectedByHeaderSubscriber * TemplateImageManager * ExternalImageCacher * TemplateImageEmbedder * Mailer * RemotePageFetcherTest * TextParserTest * MessageDataLoaderTest * MessageDataLoaderTest * Test fix * Fix: phpmd * Fix: phpcs * After review 0 * After review 1 * Add tests * EmailBuilderTest * update coderabbit.yaml * Add tests * MailSizeChecker * Feat/email building with attachments (#375) New Features PDF generation for messages, per-subscriber remote-content fetching, tracking-pixel user tracking, and richer attachment handling with downloadable copies. Improvements Unified email builder flow with consistent composition and multi-format output (HTML/Text/PDF); expanded, context-aware placeholder personalization (many URL/list resolvers); improved remote-content precaching and output formatting; new configurable parameters and translations. --------- Co-authored-by: Tatevik <tatevikg1@gmail.com> * Feat: email forwarding (#377) - Message forwarding: send campaigns to friends (optional personal note), per-user limits, admin notifications on success/failure, and forwarding statistics; forwarded messages prefixed "Fwd". - Admin-copy emails: configurable toggle to send admin copies and select recipients. --------- Co-authored-by: Tatevik <tatevikg1@gmail.com> * Cutoff from forward_email_period config * ForwardingResult * Remove MessageFormat consts * Testing bundle * After review 3 * MessageDataLoader types * Fix HTMLPurifier_Config --------- Co-authored-by: Tatevik <tatevikg1@gmail.com> * AttachmentDownloadService (#379) New Features Attachment download service that validates access, resolves files, detects MIME types, and returns downloadable content. Lightweight downloadable attachment DTO and a new exception for missing attachment files. Public constant to mark forwarded attachments. Bug Fixes Attachment download links now use a path-based format with encoded UID. * Feat: user message open tracking (#380) New Features Message view tracking: records when subscribers view messages and captures metadata (IP, User-Agent, referer). Enhancements Quick actions to mark messages as viewed and to check viewed status. Message view counters now increment when viewed. Refactor Repository lookup renamed for consistency and callers updated. Chores Switched REST API parameter to a base URL and updated tracking image path. * Remove 'to' from MessagePrecacheDto (#381) Refactor Recipient addresses are now passed explicitly throughout the email pipeline for clearer, more reliable message construction and handling. Subscriber creation now requires an email at instantiation, enforcing consistent initialization. Bug Fixes Admin notification filtering relaxed so system notifications are handled more consistently even when recipient info is missing. * Upgrade guzzle * Fix: manager configuration * Subscriber getFilteredAfterId innerJoin => lefftJoin * Add: SubscriberFilter options * Fix: subscriber prop name in query builder condition * PaginatedResult * remove sorting * Add getSubscriberDetails method * Add: front-end url * Fix: ExtraData * Add:getSummaryStatistics * Add: getCampaignPerformance, getRecentCampaigns * Ref: FilterRequestInterface * Ref: PaginatedFilter base class * Fix: types * Add: updateSubscriberList * Add: fields to export * Fix: subscriber attribute resolution * Fix: subscribers getFilteredAfterId * createSubscriptions with autoConfirm * uniqueByMessageId * todo * Fix: metadata processed * Campaign processing for handling specific lists --------- Co-authored-by: Tatevik <tatevikg1@gmail.com>
1 parent 83431b1 commit f8de2e3

290 files changed

Lines changed: 15210 additions & 1361 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coderabbit.yaml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ reviews:
88
suggested_labels: false
99
high_level_summary_in_walkthrough: false
1010
poem: false
11+
finishing_touches:
12+
docstrings:
13+
enabled: false
1114
path_instructions:
1215
- path: "src/Domain/**"
1316
instructions: |
1417
You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:
1518
16-
- ❌ Do not allow persistence or transaction side effects here for *normal* domain models.
17-
- Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:
19+
- ❌ Do not allow, flag ANY DB write / finalization:
1820
- `$entityManager->flush(...)`, `$this->entityManager->flush(...)`
19-
- `$em->persist(...)`, `$em->remove(...)`
20-
- `$em->beginTransaction()`, `$em->commit()`, `$em->rollback()`
21+
- `$em->beginTransaction()`, `$em->commit()`, `$em->rollback()`, `$em->transactional(...)`
22+
- `$em->getConnection()->executeStatement(...)` for DML/DDL (INSERT/UPDATE/DELETE/ALTER/...)
2123
- ✅ Accessing Doctrine *metadata*, *schema manager*, or *read-only schema info* is acceptable
22-
as long as it does not modify state or perform writes.
24+
as long as it does not modify state or perform writes. Accessing Doctrine *persistence APIs*
25+
persist, remove, etc.) is acceptable, allow scheduling changes in the UnitOfWork (no DB writes)
2326
2427
- ✅ **Relaxed rule for DynamicListAttr-related code**:
2528
- DynamicListAttr is a special case dealing with dynamic tables/attrs.

composer.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "phplist/core",
33
"description": "The core module of phpList, the world's most popular open source newsletter manager",
4-
"type": "phplist-module",
4+
"type": "symfony-bundle",
55
"keywords": [
66
"phplist",
77
"email",
@@ -46,6 +46,7 @@
4646
},
4747
"require": {
4848
"php": "^8.1",
49+
"symfony/framework-bundle": "^6.4",
4950
"symfony/dependency-injection": "^6.4",
5051
"symfony/config": "^6.4",
5152
"symfony/yaml": "^6.4",
@@ -79,11 +80,17 @@
7980
"ext-imap": "*",
8081
"tatevikgr/rss-feed": "dev-main",
8182
"ext-pdo": "*",
82-
"ezyang/htmlpurifier": "^4.19"
83+
"ezyang/htmlpurifier": "^4.19",
84+
"ext-libxml": "*",
85+
"ext-gd": "*",
86+
"ext-curl": "*",
87+
"ext-fileinfo": "*",
88+
"setasign/fpdf": "^1.8",
89+
"phpdocumentor/reflection-docblock": "^5.2",
90+
"guzzlehttp/guzzle": "^7.4.5"
8391
},
8492
"require-dev": {
8593
"phpunit/phpunit": "^9.5",
86-
"guzzlehttp/guzzle": "^6.3.0",
8794
"squizlabs/php_codesniffer": "^3.2.0",
8895
"phpstan/phpstan": "^1.10",
8996
"nette/caching": "^3.0.0",
@@ -92,7 +99,6 @@
9299
"symfony/test-pack": "^1.1",
93100
"symfony/process": "^6.4",
94101
"composer/composer": "^2.7",
95-
"symfony/framework-bundle": "^6.4",
96102
"symfony/http-kernel": "^6.4",
97103
"symfony/http-foundation": "^6.4",
98104
"symfony/routing": "^6.4",
@@ -152,8 +158,8 @@
152158
"Doctrine\\Bundle\\DoctrineBundle\\DoctrineBundle",
153159
"Doctrine\\Bundle\\MigrationsBundle\\DoctrineMigrationsBundle",
154160
"PhpList\\Core\\EmptyStartPageBundle\\EmptyStartPageBundle",
155-
"FOS\\RestBundle\\FOSRestBundle",
156-
"TatevikGr\\RssFeedBundle\\RssFeedBundle"
161+
"PhpList\\Core\\EmptyStartPageBundle\\PhpListCoreBundle",
162+
"FOS\\RestBundle\\FOSRestBundle"
157163
],
158164
"routes": {
159165
"homepage": {

config/PHPMD/rules.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<exclude-pattern>*/Migrations/*</exclude-pattern>
88

99
<!-- rules from the "clean code" rule set -->
10-
<rule ref="rulesets/cleancode.xml/BooleanArgumentFlag"/>
10+
<!-- <rule ref="rulesets/cleancode.xml/BooleanArgumentFlag"/>-->
1111
<rule ref="rulesets/codesize.xml/CyclomaticComplexity"/>
1212
<rule ref="rulesets/codesize.xml/NPathComplexity"/>
1313
<rule ref="rulesets/codesize.xml/ExcessiveMethodLength"/>
@@ -33,20 +33,20 @@
3333
<rule ref="rulesets/design.xml/DepthOfInheritance"/>
3434
<rule ref="rulesets/design.xml/CouplingBetweenObjects">
3535
<properties>
36-
<property name="maximum" value="15"/>
36+
<property name="maximum" value="17"/>
3737
</properties>
3838
</rule>
3939
<rule ref="rulesets/design.xml/DevelopmentCodeFragment"/>
4040

4141
<!-- rules from the "naming" rule set -->
4242
<rule ref="rulesets/naming.xml/ShortVariable">
4343
<properties>
44-
<property name="exceptions" value="id,ip,cc,io"/>
44+
<property name="exceptions" value="id,ip,cc,io,to"/>
4545
</properties>
4646
</rule>
4747
<rule ref="rulesets/naming.xml/LongVariable">
4848
<properties>
49-
<property name="maximum" value="25"/>
49+
<property name="maximum" value="30"/>
5050
</properties>
5151
</rule>
5252
<rule ref="rulesets/naming.xml/ShortMethodName"/>

config/PhpCodeSniffer/ruleset.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
<rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/>
104104
<rule ref="Squiz.WhiteSpace.CastSpacing"/>
105105
<rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/>
106-
<rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
106+
<rule ref="Squiz.WhiteSpace.OperatorSpacing">
107+
<properties>
108+
<property name="ignoreNewlines" value="true"/>
109+
</properties>
110+
</rule>
107111
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
108112
</ruleset>

config/parameters.yml.dist

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,28 @@ parameters:
2525
env(DATABASE_PREFIX): 'phplist_'
2626
list_table_prefix: '%%env(LIST_TABLE_PREFIX)%%'
2727
env(LIST_TABLE_PREFIX): 'listattr_'
28+
app.dev_version: '%%env(APP_DEV_VERSION)%%'
29+
env(APP_DEV_VERSION): '0'
30+
app.dev_email: '%%env(APP_DEV_EMAIL)%%'
31+
env(APP_DEV_EMAIL): 'dev@dev.com'
32+
app.powered_by_phplist: '%%env(APP_POWERED_BY_PHPLIST)%%'
33+
env(APP_POWERED_BY_PHPLIST): '0'
34+
app.preference_page_show_private_lists: '%%env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS)%%'
35+
env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS): '0'
36+
app.rest_api_base_url: '%%env(REST_API_BASE_URL)%%'
37+
env(REST_API_BASE_URL): 'http://api.phplist.local/api/v2'
38+
app.frontend_base_url: '%%env(FRONT_END_BASE_URL)%%'
39+
env(FRONT_END_BASE_URL): 'http://frontend.phplist.local'
2840

2941
# Email configuration
3042
app.mailer_from: '%%env(MAILER_FROM)%%'
3143
env(MAILER_FROM): 'noreply@phplist.com'
3244
app.mailer_dsn: '%%env(MAILER_DSN)%%'
33-
env(MAILER_DSN): 'null://null'
45+
env(MAILER_DSN): 'null://null' # set local_domain on transport
3446
app.confirmation_url: '%%env(CONFIRMATION_URL)%%'
35-
env(CONFIRMATION_URL): 'https://example.com/subscriber/confirm/'
47+
env(CONFIRMATION_URL): 'http://api.phplist.local/api/v2/subscriber/confirm/'
3648
app.subscription_confirmation_url: '%%env(SUBSCRIPTION_CONFIRMATION_URL)%%'
37-
env(SUBSCRIPTION_CONFIRMATION_URL): 'https://example.com/subscription/confirm/'
49+
env(SUBSCRIPTION_CONFIRMATION_URL): 'http://api.phplist.local/api/v2/subscription/confirm/'
3850
app.password_reset_url: '%%env(PASSWORD_RESET_URL)%%'
3951
env(PASSWORD_RESET_URL): 'https://example.com/reset/'
4052

@@ -71,8 +83,10 @@ parameters:
7183
# A secret key that's used to generate certain security-related tokens
7284
secret: '%%env(PHPLIST_SECRET)%%'
7385
env(PHPLIST_SECRET): %1$s
86+
phplist.verify_ssl: '%%env(VERIFY_SSL)%%'
87+
env(VERIFY_SSL): '1'
7488

75-
graylog_host: 'graylog.example.com'
89+
graylog_host: 'graylog.phplist.local'
7690
graylog_port: 12201
7791

7892
app.phplist_isp_conf_path: '%%env(APP_PHPLIST_ISP_CONF_PATH)%%'
@@ -89,3 +103,57 @@ parameters:
89103
env(MESSAGING_MAX_PROCESS_TIME): '600'
90104
messaging.max_mail_size: '%%env(MAX_MAILSIZE)%%'
91105
env(MAX_MAILSIZE): '209715200'
106+
messaging.default_message_age: '%%env(DEFAULT_MESSAGEAGE)%%'
107+
env(DEFAULT_MESSAGEAGE): '691200'
108+
messaging.use_manual_text_part: '%%env(USE_MANUAL_TEXT_PART)%%'
109+
env(USE_MANUAL_TEXT_PART): '0'
110+
messaging.blacklist_grace_time: '%%env(MESSAGING_BLACKLIST_GRACE_TIME)%%'
111+
env(MESSAGING_BLACKLIST_GRACE_TIME): '600'
112+
messaging.google_sender_id: '%%env(GOOGLE_SENDERID)%%'
113+
env(GOOGLE_SENDERID): ''
114+
messaging.use_amazon_ses: '%%env(USE_AMAZONSES)%%'
115+
env(USE_AMAZONSES): '0'
116+
messaging.use_precedence_header: '%%env(USE_PRECEDENCE_HEADER)%%'
117+
env(USE_PRECEDENCE_HEADER): '0'
118+
messaging.embed_external_images: '%%env(EMBEDEXTERNALIMAGES)%%'
119+
env(EMBEDEXTERNALIMAGES): '0'
120+
messaging.embed_uploaded_images: '%%env(EMBEDUPLOADIMAGES)%%'
121+
env(EMBEDUPLOADIMAGES): '0'
122+
messaging.external_image_max_age: '%%env(EXTERNALIMAGE_MAXAGE)%%'
123+
env(EXTERNALIMAGE_MAXAGE): '0'
124+
messaging.external_image_timeout: '%%env(EXTERNALIMAGE_TIMEOUT)%%'
125+
env(EXTERNALIMAGE_TIMEOUT): '30'
126+
messaging.external_image_max_size: '%%env(EXTERNALIMAGE_MAXSIZE)%%'
127+
env(EXTERNALIMAGE_MAXSIZE): '204800'
128+
messaging.forward_alternative_content: '%%env(FORWARD_ALTERNATIVE_CONTENT)%%'
129+
env(FORWARD_ALTERNATIVE_CONTENT): '0'
130+
messaging.email_text_credits: '%%env(EMAILTEXTCREDITS)%%'
131+
env(EMAILTEXTCREDITS): '0'
132+
messaging.always_add_user_track: '%%env(ALWAYS_ADD_USERTRACK)%%'
133+
env(ALWAYS_ADD_USERTRACK): '1'
134+
messaging.send_list_admin_copy: '%%env(SEND_LISTADMIN_COPY)%%'
135+
env(SEND_LISTADMIN_COPY): '0'
136+
137+
phplist.forward_email_period: '%%env(FORWARD_EMAIL_PERIOD)%%'
138+
env(FORWARD_EMAIL_PERIOD): '1 minute'
139+
phplist.forward_email_count: '%%env(FORWARD_EMAIL_COUNT)%%'
140+
env(FORWARD_EMAIL_COUNT): '1'
141+
phplist.forward_personal_note_size: '%%env(FORWARD_PERSONAL_NOTE_SIZE)%%'
142+
env(FORWARD_PERSONAL_NOTE_SIZE): '0'
143+
phplist.forward_friend_count_attribute: '%%env(FORWARD_FRIEND_COUNT_ATTRIBUTE)%%'
144+
env(FORWARD_FRIEND_COUNT_ATTRIBUTE): ''
145+
phplist.keep_forwarded_attributes: '%%env(KEEPFORWARDERATTRIBUTES)%%'
146+
env(KEEPFORWARDERATTRIBUTES): '0'
147+
148+
phplist.upload_images_dir: '%%env(PHPLIST_UPLOADIMAGES_DIR)%%'
149+
env(PHPLIST_UPLOADIMAGES_DIR): 'images'
150+
phplist.editor_images_dir: '%%env(FCKIMAGES_DIR)%%'
151+
env(FCKIMAGES_DIR): 'uploadimages'
152+
phplist.public_schema: '%%env(PUBLIC_SCHEMA)%%'
153+
env(PUBLIC_SCHEMA): 'https'
154+
phplist.attachment_download_url: '%%env(PHPLIST_ATTACHMENT_DOWNLOAD_URL)%%'
155+
env(PHPLIST_ATTACHMENT_DOWNLOAD_URL): 'https://example.com/download/'
156+
phplist.attachment_repository_path: '%%env(PHPLIST_ATTACHMENT_REPOSITORY_PATH)%%'
157+
env(PHPLIST_ATTACHMENT_REPOSITORY_PATH): '/tmp'
158+
phplist.max_avatar_size: '%%env(MAX_AVATAR_SIZE)%%'
159+
env(MAX_AVATAR_SIZE): '100000'

config/services.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,9 @@ services:
5757
calls:
5858
- [ set, [ 'Cache.SerializerPath', '%kernel.cache_dir%/htmlpurifier' ] ]
5959
- [ set, [ 'HTML.ForbiddenElements', [ 'script', 'style' ] ] ]
60-
- [ set, [ 'CSS.Disable', true ] ]
61-
- [ set, [ 'URI.DisableJavaScript', true ] ]
62-
- [ set, [ 'URI.DisableDataURI', true ] ]
63-
- [ set, [ 'HTML.Doctype', 'HTML5' ] ]
60+
- [ set, [ 'CSS.AllowedProperties', [] ] ]
61+
- [ set, [ 'URI.AllowedSchemes', { http: true, https: true, mailto: true } ] ]
62+
- [ set, [ 'HTML.Doctype', 'XHTML 1.0 Transitional' ] ]
6463
- [ set, [ 'HTML.Allowed', 'p,br,b,strong,i,em,u,a[href|title],ul,ol,li,blockquote,img[src|alt|title],span,div'] ]
6564

6665
HTMLPurifier:

config/services/builders.yml

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,34 @@ services:
44
autoconfigure: true
55
public: false
66

7-
PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder:
8-
autowire: true
9-
autoconfigure: true
7+
PhpList\Core\Domain\:
8+
resource: '../../src/Domain/*/Service/Builder/*'
109

11-
PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder:
12-
autowire: true
13-
autoconfigure: true
10+
# Concrete mail constructors
11+
PhpList\Core\Domain\Messaging\Service\Constructor\SystemMailContentBuilder: ~
12+
PhpList\Core\Domain\Messaging\Service\Constructor\CampaignMailContentBuilder: ~
1413

15-
PhpList\Core\Domain\Messaging\Service\Builder\MessageScheduleBuilder:
16-
autowire: true
17-
autoconfigure: true
14+
# Two EmailBuilder services with different constructors injected
15+
PhpList\Core\Domain\Messaging\Service\Builder\SystemEmailBuilder:
16+
arguments:
17+
$googleSenderId: '%messaging.google_sender_id%'
18+
$useAmazonSes: '%messaging.use_amazon_ses%'
19+
$usePrecedenceHeader: '%messaging.use_precedence_header%'
20+
$devVersion: '%app.dev_version%'
21+
$devEmail: '%app.dev_email%'
1822

19-
PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder:
20-
autowire: true
21-
autoconfigure: true
23+
PhpList\Core\Domain\Messaging\Service\Builder\EmailBuilder:
24+
arguments:
25+
$googleSenderId: '%messaging.google_sender_id%'
26+
$useAmazonSes: '%messaging.use_amazon_ses%'
27+
$usePrecedenceHeader: '%messaging.use_precedence_header%'
28+
$devVersion: '%app.dev_version%'
29+
$devEmail: '%app.dev_email%'
2230

23-
PhpList\Core\Domain\Messaging\Service\Builder\MessageOptionsBuilder:
24-
autowire: true
25-
autoconfigure: true
31+
PhpList\Core\Domain\Messaging\Service\Builder\ForwardEmailBuilder:
32+
arguments:
33+
$googleSenderId: '%messaging.google_sender_id%'
34+
$useAmazonSes: '%messaging.use_amazon_ses%'
35+
$usePrecedenceHeader: '%messaging.use_precedence_header%'
36+
$devVersion: '%app.dev_version%'
37+
$devEmail: '%app.dev_email%'

0 commit comments

Comments
 (0)