Add premium logo and professional theme for high-end clients

- Create custom SVG logo with professional branding
- Implement premium color scheme with blue and gold accents
- Add custom CSS with professional styling for cards, tables, buttons
- Update logo template to use new logo.svg file
- Create custom favicon for complete branding
- Redesign homepage with premium content sections
- Update resources page with membership tiers and premium pricing
- Enhance contact page with testimonials and detailed information
- Target audience: high-paying clients ($100+/hour)
- Professional yet approachable design language

💘 Generated with Crush

Assisted-by: GLM-4.7 via Crush <crush@charm.land>
This commit is contained in:
Charles N Wyble
2026-01-13 16:15:40 -05:00
parent e6c15cafb3
commit 9f7fe553dc
2596 changed files with 433475 additions and 113 deletions

View File

@@ -0,0 +1,428 @@
# v4.2.0
## 08/25/2025
1. [](#new)
* PHP 8.4 compatibility
1. [](#improved)
* Vendor library updates
# v4.1.2
## 01/06/2025
1. [](#improved)
* Updated to latest symfony libraries (`v5.4.45`)
* Added a note about Google App Passwords for authentication in the README.md
# v4.1.1
## 10/28/2024
1. [](#improved)
* Updated logging methods to support Monolog2+
# v4.1.0
## 04/09/2024
1. [](#improved)
* Update vendor libraries for Mailer to latest `5.4` versions
* When you send an email you can use `Email::getLastSendMessage()` and `Email::getLastSendDebug()` methods to get information about the email processing.
# v4.0.4
## 07/10/2023
1. [](#bugfix)
* Fix for email file attachments using stream
# v4.0.3
## 06/29/2023
1. [](#improved)
* Simplified the `Email::processRecipients()` logic for readability
1. [](#bugfix)
* Fix an issue with 2 email addresses provided with 'just' email and no name [#176](https://github.com/getgrav/grav-plugin-email/issues/176)
* Fix for blank subjectlines when using `Message::setSubject()` in Twig templates [getgrav/grav-plugin-login#299](https://github.com/getgrav/grav-plugin-login/issues/299)
# v4.0.2
## 06/27/2023
1. [](#bugfix)
* some recipient handling improvements. e.g. missing `bcc_name` throwing error
* Allow overriding of defaults with a form configuration. Use `null` to remove default email configuration
# v4.0.1
## 05/20/2023
1. [](#bugfix)
* Fixed a bug with Email 4.0 ignoring `template:` parameter when dealing with multiple body sections
# v4.0.0
## 05/09/2023
1. [](#new)
* Migrated from `Swiftmailer` (which has been deprecated) to `Symfony/Mailer`, a more modern and extensible mailing library.
* Built-in support for `SMTP`, `SMTPS`, `Sendmail` and `Native` (uses best solution per-platform) engines
* Added ability to have 3rd party plugins that provide new engines (e.g. `SendGrid`, `MailJet`, etc.) with `SMTP`, `API` or `HTTPS` transports for faster and more reliable email sending
* More flexible options for email formatting including RFC822 `name-addr` format (`Joe Bloggs <user@example.com>`) and simple arrays (`[user@example.com, Joe Bloggs]`)
* Support `tags` and `metadata` in `Email::buildMessage()`
* Two new events `onEmailEngines` and `onEmailTransportDsn` to facilitate the integration of custom engines via plugins
# v4.0.0-rc.4
## 02/27/2023
1. [](#bugfix)
* Fixed for multiple recipients [#167](https://github.com/getgrav/grav-plugin-email/issues/167)
* Fix for simple array format with names which wasn't working
# v4.0.0-rc.3
## 10/27/2022
1. [](#bugfix)
* Fixed an issue with array based email address
# v4.0.0-rc.2
## 10/19/2022
1. [](#improved)
* Set `sendmail` as default engine to provide better fallback for unsupported `mailer` option
* Added info about available engine plugins in `README.md`
# v4.0.0-rc.1
## 10/05/2022
1. [](#new)
* Migrated from `Swiftmailer` (which has been deprecated) to `Symfony/Mailer`, a more modern and extensible mailing library.
* Built-in support for `SMTP`, `SMTPS`, `Sendmail` and `Native` (uses best solution per-platform) engines
* Added ability to have 3rd party plugins that provide new engines (e.g. `SendGrid`, `MailJet`, etc.) with `SMTP`, `API` or `HTTPS` transports for faster and more reliable email sending
* More flexible options for email formatting including RFC822 `name-addr` format (`Joe Bloggs <user@example.com>`) and simple arrays (`[user@example.com, Joe Bloggs]`)
* Support `tags` and `metadata` in `Email::buildMessage()`
* Two new events `onEmailEngines` and `onEmailTransportDsn` to facilitate the integration of custom engines via plugins
# v3.2.0
## 03/28/2022
1. [](#new)
* Require **Grav 1.7.32** and **Form 6.0.0**
1. [](#improved)
* Added access email message object inside the twig template file
2. [](#bugfix)
* Fixed twig not being initialized when building an email message
# v3.1.5
## 01/03/2022
1. [](#improved)
* Updated to Swiftmailer `v6.3.0` with PHP 8.1 compatibility [#157](https://github.com/getgrav/grav-plugin-email/issues/157)
# v3.1.4
## 11/16/2021
1. [](#improved)
* Added second parameter to `Email::send()` to get failed recipients
# v3.1.3
## 07/19/2021
1. [](#improved)
* Pass page variable to processed forms [#141](https://github.com/getgrav/grav-plugin-email/pull/141)
* Email configuration available to templates [#152](https://github.com/getgrav/grav-plugin-email/pull/152)
* New Event after eMail was sent [#151](https://github.com/getgrav/grav-plugin-email/pull/151)
# v3.1.2
## 04/06/2021
1. [](#new)
* Added new `onEmailMessage` event to make object available for editing [#150](https://github.com/getgrav/grav-plugin-email/pull/150)
# v3.1.1
## 01/31/2021
1. [](#improved)
* Latest vendor updates including SwiftMailer `6.2.5`
* Updated CLI commands
* Minor code cleanup
# v3.1.0
## 12/02/2020
1. [](#improved)
* Added support for `auth_mode` in SMTP engine [#101](https://github.com/getgrav/grav-plugin-email/pull/101)
* Obfuscate the password shown in the CLI `test-email` command [#140](https://github.com/getgrav/grav-plugin-email/pull/140)
# v3.0.10
## 11/09/2020
1. [](#improved)
* Tweaked default `base.html.twig` template to better support dark-mode clients
* Latest vendor updates
1. [](#bugfix)
* Add missing support for `template:` in body array
* Added check to process markdown with `text/html` content type only
# v3.0.9
## 06/08/2020
1. [](#improved)
* Disable password autocomplete in password field
* Don't save empty string in password field [#134](https://github.com/getgrav/grav-plugin-email/issues/134)
# v3.0.8
## 04/27/2020
1. [](#improved)
* Updated vendor library files
* Use Grav's Parsedown class
# v3.0.7
## 03/05/2020
1. [](#improved)
* Updated email validator library
1. [](#bugfix)
* Fixed `Invalid resource theme://` on CLI command `test-email` on Grav 1.6.21 and later versions [#128](https://github.com/getgrav/grav-plugin-email/issues/128)
# v3.0.6
## 02/11/2020
1. [](#improved)
* Updated email validator library
# v3.0.5
## 02/03/2020
1. [](#bugfix)
* Fixed a date in changelog (no other changes)
# v3.0.4
## 01/17/2020
1. [](#improved)
* Added ZOHO configuration example
* Updated SwiftMailer library for PHP 7.4 support
# v3.0.3
## 08/16/2019
1. [](#new)
* Support an array of multiple emails in `email:` form process
* Allow form values in email templates
1. [](#improved)
* Added Twig blocks for `content` and `footer` in `email/base.html.twig` template
* Updated `README.md` to reflect working setup for GMail
# v3.0.2
## 05/09/2019
1. [](#new)
* Requires Form Plugin v3.0.3
* Added Russian translation [#113](https://github.com/getgrav/grav-plugin-email/pull/113)
1. [](#bugfix)
* Better fix for missing attachments when sending an email using a form [form#333](https://github.com/getgrav/grav-plugin-form/issues/333)
# v3.0.1
## 04/15/2019
1. [](#improved)
* Put a `try/catch` around email attachments and log any errors rather than hard fail
1. [](#bugfix)
* Fixed missing attachments when sending an email using a form [form#333](https://github.com/getgrav/grav-plugin-form/issues/333)
# v3.0.0
## 04/11/2019
1. [](#new)
* Added new `template:` to choose twig template option for email form processing
* Moved `buildMessage()` and `parseAddressValue()` to Email object and made public
* Refactored the `EmailUtils::sendEmail()` to take an array of params or the old param list
* Switched to SwiftMailer v.6.1.3 (requires PHP7/Grav 1.6)
* SwiftMailer 6.x compatibility fixes
* Updated various translations
* Added support for Email Queue with Scheduler support
* Code cleanup, composer update
* Added a new `clear-queue-failures` CLI command to flush out failed sends
1. [](#improved)
* Added backlink for scheduler task
* Added support for `environment` option to `flushqueue` CLI command
* Fixed mailtrap hostname in README.md
* Disable autocomplete on SMTP `user` and `password` fields
# v2.7.2
## 01/25/2019
1. [](#improved)
* Added default for `to` address
* Updated EN language [#99](https://github.com/getgrav/grav-plugin-email/pull/99)
* Updated UK language [#98](https://github.com/getgrav/grav-plugin-email/pull/98)
* Updated RU language [#100](https://github.com/getgrav/grav-plugin-email/pull/100)
* Updated to SwiftMailer v5.4.12
1. [](#bugfix)
* Fixed `mailtrap` hostname
# v2.7.1
## 12/05/2017
1. [](#new)
* Added new `onEmailSend()` event hook before sending [#70](https://github.com/getgrav/grav-plugin-email/pull/70)
1. [](#improved)
* Added examples of setting up Email plugin with various SMTP providers
* Updated RU language [#60](https://github.com/getgrav/grav-plugin-email/pull/60)
* Updated to SwiftMailer v5.4.8
# v2.7.0
## 10/26/2017
1. [](#improved)
* Now uses a dedicated `logs/email.log` file when `debug: true`
* Improved the README.txt file with examples, and troubleshooting
* Changed default engine to `sendmail` as `mail` is deprecated and not functioning [swiftmailer#866](https://github.com/swiftmailer/swiftmailer/issues/866}
# v2.6.2
## 09/30/2017
1. [](#improved)
* Removed extraneous files from vendor folder
# v2.6.1
## 09/07/2017
1. [](#improved)
* Improved the error message when missing `from` in the configuration
* Silently catch malformed email exceptions
# v2.6.0
## 05/22/2017
1. [](#improved)
* Inherit options from plugin configuration [#39](https://github.com/getgrav/grav-plugin-email/pull/39)
1. [](#bugfix)
* Also process translation on the email subject [https://github.com/getgrav/grav-plugin-comments/issues/38](https://github.com/getgrav/grav-plugin-comments/issues/38)
# v2.5.3
## 01/03/2017
1. [](#improved)
* Updated to SwiftMailer 5.4.5 [#45](https://github.com/getgrav/grav-plugin-email/issues/45)
# v2.5.2
## 12/13/2016
1. [](#new)
* RC released as stable
# v2.5.2-rc.1
## 11/26/2016
1. [](#new)
* Added a new `process_markdown` option for emails in forms
1. [](#improved)
* Improved the `Utils::sendEmail()` method to take the email type as an option
# v2.5.1
## 10/19/2016
1. [](#improved)
* CLI command will fallback to use the `to` from email plugin config if not provided
* Explicit Composer based class loader to fix issues with class case
# v2.5.0
## 09/07/2016
1. [](#new)
* Added a new `bin/plugin email test-email` CLI command
1. [](#improved)
* Moved Email `Utils` class from Login to Email plugin
* Provide a sample base `email/base.html.twig` template for emails
1. [](#bugfix)
* Fix handling attachments with the updated file upload field
# v2.4.3
## 08/16/2016
1. [](#improved)
* Added Russian translation
* Updated Swiftmailer to 5.4.3 [#37](https://github.com/getgrav/grav-plugin-email/issues/37)
# v2.4.2
## 08/10/2016
1. [](#improved)
* Added Croatian translation
# v2.4.1
## 07/14/2016
1. [](#improved)
* Allow multiple email recipients (comma separated) [#31](https://github.com/getgrav/grav-plugin-email/issues/31)
* Added Danish and Spanish translations
# v2.4.0
## 05/11/2016
1. [](#improved)
* Now includes Swiftmailer v5.4.2 which introduces a number of bug fixes and improvements
1. [](#bugfix)
* Correct `starttls` implementation, bundled in TLS
# v2.3.0
## 04/20/2016
1. [](#improved)
* Added debug option to enable logging on SwiftMailer.
* Updated SwiftMailer from v5.1.0 to v5.4.1.
* Added an option in the Admin settings to enable `starttls`
1. [](#bugfix)
* Correctly name TLS in the Admin settings, the label was `TTS` (but the value was correctly named `tls`)
# v2.2.0
## 02/05/2016
1. [](#new)
* Allow to send attachments in forms
* Added French translation
1. [](#improved)
* Throw an exception when trying to send emails without a `from` or `to` parameters setup, to intercept less meaningful errors and provide a better description on how to fix the problem
* Changed SMTP password in admin to use a password field instead of plain text
# v2.1.0
## 12/18/2015
1. [](#new)
* Added missing `content_type` to email.yaml
* Added default values for CC and BCC
1. [](#improved)
* Improved documentation of new email params in `README.md`
* Moved config setting of `mailer.default` to `mailer.engine`
# v2.0.0
## 12/11/2015
1. [](#new)
* Added support for from/sender name (Thomas Keitel)
* Added support for message content type (Thomas Keitel)
* Added support for reply addresses (Thomas Keitel)
* Added support for CC/BCC (Thomas Keitel)
* Added support for multiple body parts (Thomas Keitel)
1. [](#bugfix)
* Fix email engine selection (z38)
# v1.0.0
## 11/20/2015
1. [](#bugfix)
* Fix for issue with no body parameter specified
# v0.2.1
## 09/11/2015
1. [](#bugfix)
* Fix onFormProcessed event
# v0.2.0
## 08/11/2015
1. [](#improved)
* Disable `enable` in admin
# v0.1.0
## 08/04/2015
1. [](#new)
* ChangeLog started...

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Grav
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,504 @@
# Grav Email Plugin
The **email plugin** for [Grav](http://github.com/getgrav/grav) adds the ability to send email utilizing the `symfony/mailer` package. This is particularly useful for the **admin** and **login** plugins.
> IMPORTANT: **Version 4.0** replaced the old deprecated **SwiftMailer** library with **Symfony/Mailer** package. This is a modern and well supported library that also has the capability to support 3rd party transport engines such as `SendGrid`, `MailJet`, `MailGun`, `MailChimp`, etc. This library should be backwards compatible with existing configurations, but please create an issue if you run into any problems.
# Installation
The email plugin is easy to install with GPM.
```
$ bin/gpm install email
```
# Configuration
The plugin uses `sendmail` binary as the default mail engine.
```
enabled: true
from:
to:
mailer:
engine: sendmail
smtp:
server: localhost
port: 25
encryption: none
user:
password:
sendmail:
bin: '/usr/sbin/sendmail -bs'
content_type: text/html
debug: false
```
You can configure the Email plugin by using the Admin plugin, navigating to the Plugins list and choosing `Email`.
That's the easiest route. Or you can also alter the Plugin configuration by copying the `user/plugins/email/email.yaml` file into `user/config/plugins/email.yaml` and make your modifications there.
The first setting you'd likely change is your `Email from` / `Email to` names and emails.
Also, you'd likely want to setup a SMTP server instead of using PHP Mail, as the latter is not 100% reliable and you might experience problems with emails.
## Built-in Engines
By default Email 4.0 supports 4 native engines:
* SMTP - Standard "Simple Mail Transport Protocol" - The default for most providers
* SMTPS - "Simple Mail Transport Protocol Secure" - Not very commonly used
* Sendmail - Uses the built-in `sendmail` binary file available on many Linux and Mac systems
* Native - Uses `sendmail_path` of `php.ini` for Mac + Linux, and `smtp` and `smtp_port` on Windows
Due to the modular nature of Symfony/Mailer, 3rd party engines are supported via Grav plugins.
## 3rd-Party Engines Plugin Support
Along with the **Email** `v4.0` release, there has also been several custom provider plugins released to provide support for `SMTP`, `API`, and sometimes even `HTTPS` support for 3rd party providers such as **Sendgrid**, **MailJet**, **MailGun**, **Amazon SES**, **Mailchimp/Mandrill**, and others! `API` or `HTTPS` will provide a faster email sending experience compared to `SMTP` which is an older protocol and requires more back-and-forth negotiation and communication compared to the single-request of `API` or `HTTPS` solutions.
Examples of the currently available plugins include:
* https://github.com/getgrav/grav-plugin-email-sendgrid - Sengrid Mailer
* https://github.com/getgrav/grav-plugin-email-amazon - Amazon SES
* https://github.com/getgrav/grav-plugin-email-mandrill - Mailchimp Mandrill Mailer
* https://github.com/getgrav/grav-plugin-email-mailersend - Mailersend Mailer
More plugins will be released soon to support `Gmail`, `Mailgun`, `Mailjet`, `OhMySMTP`, `Postmark`, and `SendInBlue`.
## SMTP Configurations for popular solutions:
### Google Email
A popular option for sending email is to simply use your Google Accounts SMTP server. To set this up you will need to do 2 things first:
As Gmail no longer supports the "allow less secure apps" option, you now need to have 2FA enabled on the account and setup an "App Password" to create a specific password rather than your general account password. Follow these instructions: [https://support.google.com/accounts/answer/185833](https://support.google.com/accounts/answer/185833).
!! Important: When Google creates an app password for you it will look something like `pxzl vkxd xdap fomb`, you should make sure to remove the spaces when you store your password so it looks like `pxzlvkxdxdapfomb`. Also make sure you are sending from the same email address as the one you are using to authenticate with.
Then configure the Email plugin:
```
mailer:
engine: smtp
smtp:
server: smtp.gmail.com
port: 587
user: 'YOUR_GOOGLE_EMAIL_ADDRESS'
password: 'YOUR_GOOGLE_PASSWORD'
```
> NOTE: Check your email sending limits: https://support.google.com/a/answer/166852?hl=en
### Mailtrap.io
A good way to test emails is to use a SMTP server service that's built for testing emails, for example [https://mailtrap.io](https://mailtrap.io)
Setup the Email plugin to use that SMTP server with the fake inbox data. For example enter this configuration in `user/config/plugins/email.yaml` or through the Admin panel:
```
mailer:
engine: smtp
smtp:
server: smtp.mailtrap.io
port: 2525
encryption: none
user: YOUR_MAILTRAP_INBOX_USER
password: YOUR_MAILTRAP_INBOX_PASSWORD
```
That service will intercept emails and show them on their web-based interface instead of sending them for real.
You can try and fine tune the emails there while testing.
### Sparkpost
Generous email sending limits even in the free tier, and simple setup, make [Sparkpost](https://www.sparkpost.com) a great option for email sending. You just need to create an account, then setup a verified sending domain. Sparkpost does a nice job of making this process very easy and undertandable. Then just click on the SMTP Relay option to get your details for the configuration:
```
mailer:
engine: smtp
smtp:
server: smtp.sparkpostmail.com
port: 587
user: 'SMTP_Injection'
password: 'SEND_EMAIL_API_KEY'
```
Then try sending a test email...
### Sendgrid
[Sendgrid](https://sendgrid.com) offers a very easy-to-setup serivce with 100 emails/day for free. The next level allows you to send 40k/email a day for just $10/month. Configuration is pretty simple, just create an account, then click SMTP integration and click the button to create an API key. The configuration is as follows:
```
mailer:
engine: smtp
smtp:
server: smtp.sendgrid.net
port: 587
user: 'apikey'
password: 'YOUR_SENDGRID_API_KEY'
```
### Mailgun
[Mailgun is a great service](https://www.mailgun.com/) that offers 10k/emails per month for free. Setup does require SPIF domain verification so that means you need to add at least a TXT entry in your DNS. This is pretty standard for SMTP sending services and does provide verification for remote email servers and makes your email sending more reliable. The Mailgun site, walks you through this process however, and the verification process is simple and fast.
```
mailer:
engine: smtp
smtp:
server: smtp.mailgun.org
port: 587
user: 'MAILGUN_EMAIL_ADDRESS'
password: 'MAILGUN_EMAIL_PASSWORD'
```
Adjust these configurations for your account.
### MailJet
Mailjet is another great service that is easy to quickly setup and get started sending email. The free account gives you 200 emails/day or 600 emails/month. Just signup and setup your SPF and DKIM entries for your domain. Then click on the SMTP settings and use those to configure the email plugin:
```
mailer:
engine: smtp
smtp:
server: in-v3.mailjet.com
port: 587
user: 'MAILJUST_USERNAME_API_KEY'
password: 'MAILJUST_PASSWORD_SECRET_KEY'
```
### ZOHO
ZOHO is a popular solution for hosted email due to it's great 'FREE' tier. It's paid options are also very reasonable and combined with the latest UI updates and outstanding security features, it's a solid email option.
In order to get ZOHO working with Grav, you need to send email via a user account. You can either use your users' password or generate an **App Password** via your ZOHO account (clicking on your avatar once logged in), then navigating to `My Account -> Security -> App Passwords -> Generate`. Just enter a unique app name (i.e. `Grav Website`).
NOTE: The SMTP host required can be found in `Settings -> Mail - > Mail Accounts -> POP/IMAP -> SMTP`. This will provide the SMTP server for this account (it may not be `imap.zoho.com` depending on what region you are in)
```
mailer:
engine: smtp
smtp:
server: smtp.zoho.com
port: 587
user: 'ZOHO_EMAIL_ADDRESS'
password: 'ZOHO_EMAIL_PASSWORD'
```
### Sendmail
Although not as reliable as SMTP not providing as much debug information, sendmail is a simple option as long as your hosting provider is not blocking the default SMTP port `25`:
```
mailer:
engine: sendmail
sendmail:
bin: '/usr/sbin/sendmail -bs'
```
Simply adjust your binary command line to suite your environment
## SMTP Email Services
Solid SMTP options that even provide a FREE tier for low email volumes include:
* SendGrid (100/day free) - https://sendgrid.com
* Mailgun - (10k/month free) - https://www.mailgun.com
* Mailjet - (6k/month free) - https://www.mailjet.com/
* Sparkpost - (15k/month free) - https://www.sparkpost.com
* Amazon SES (62k/month free) - https://aws.amazon.com/ses/
If you are still unsure why should be using one in the first place, check out this article: https://zapier.com/learn/email-marketing/best-transactional-email-sending-services/
## Testing with CLI Command
You can test your email configuration with the following CLI Command:
```
$ bin/plugin email test-email -t test@email.com
```
You can also pass in a configuration environment:
```
$ bin/plugin email test-email -t test@email.com --env=mysite.com
```
This will use the email configuration you have located in `user/mysite.com/config/plugins/email.yaml`. Read the docs to find out more about environment-based configuration: https://learn.getgrav.org/advanced/environment-config
# Programmatically send emails
Add this code in your plugins:
```php
$to = 'email@test.com';
$from = 'email@test.com';
$subject = 'Test';
$content = 'Test';
$message = $this->grav['Email']->message($subject, $content, 'text/html')
->setFrom($from)
->setTo($to);
$sent = $this->grav['Email']->send($message);
```
# Emails sent with Forms
When executing email actions during form processing, action parameters are inherited from the global configuration but may also be overridden on a per-action basis.
```
title: Custom form
form:
name: custom_form
fields:
# Any fields you'd like to add to the form:
# Their values may be referenced in email actions via '{{ form.value.FIELDNAME|e }}'
process:
email:
subject: "[Custom form] {{ form.value.name|e }}"
body: "{% include 'forms/data.txt.twig' %}"
from: Custom Sender <sender@example.com>
to: Custom Recipient <recipient@example.com>
process_markdown: true
```
## Multiple Emails
You can send multiple emails by creating an array of emails under the `process: email:` option in the form:
```
title: Custom form
form:
name: custom_form
fields:
# Any fields you'd like to add to the form:
# Their values may be referenced in email actions via '{{ form.value.FIELDNAME|e }}'
process:
email:
-
subject: "[Custom Email 1] {{ form.value.name|e }}"
body: "{% include 'forms/data.txt.twig' %}"
from: Site Owner <owner@mysite.com>
to: Recipient 1 <recepient_1@example.com>
template: "email/base.html.twig"
-
subject: "[Custom Email 2] {{ form.value.name|e }}"
body: "{% include 'forms/data.txt.twig' %}"
from: Site Owner <owner@mysite.com>
to: Recipient 2 <recepient_2@example.com>
template: "email/base.html.twig"
```
## Templating Emails
You can specify a Twig template for HTML rendering, else Grav will use the default one `email/base.html.twig` which is included in this plugin. You can also specify a custom template that extends the base, where you can customize the `{% block content %}` and `{% block footer %}`. For example:
```twig
{% extends 'email/base.html.twig' %}
{% block content %}
<p>
Greetings {{ form.value.name|e }},
</p>
<p>
We have received your request for help. Our team will get in touch with you within 3 Business Days.
</p>
<p>
Regards,
</p>
<p>
<b>My Company</b>
<br /><br />
E - <a href="mailto:help@mycompany.com">help@mycompany.com</a><br />
M - +1 555-123-4567<br />
W - <a href="https://mycompany.com">mycompany.com</a>
</p>
{% endblock %}
{% block footer %}
<p style="text-align: center;">My Company - All Rights Reserved</p>
{% endblock %}
```
## Sending Attachments
You can add file inputs to your form, and send those files via Email.
Just add an `attachments` field and list the file input fields names. You can have multiple file fields, and this will send all the files as attachments. Example:
```
form:
name: custom_form
fields:
my-file:
label: 'Add a file'
type: file
multiple: false
destination: user/data/files
accept:
- application/pdf
- application/x-pdf
- image/png
- text/plain
process:
email:
body: '{% include "forms/data.html.twig" %}'
attachments:
- 'my-file'
```
## Additional action parameters
To have more control over your generated email, you may also use the following additional parameters:
* `reply_to`: Set one or more addresses that should be used to reply to the message.
* `cc` _(Carbon copy)_: Add one or more addresses to the delivery list. Many email clients will mark email in one's inbox differently depending on whether they are in the `To:` or `Cc:` list.
* `bcc` _(Blind carbon copy)_: Add one or more addresses to the delivery list that should (usually) not be listed in the message data, remaining invisible to other recipients.
### Specifying email addresses
Email-related parameters (`from`, `to`, `reply_to`, `cc`and `bcc`) allow different notations for single / multiple values:
#### Single email address string
```
to: mail@example.com
```
#### `name-addr` RFC822 Formatted string
```
to: Joe Bloggs <maiil@example.com>
```
#### Multiple email address strings
```
to:
- mail@example.com
- mail+1@example.com
- mail+2@example.com
```
or in `name-addr` format:
```
to:
- Joe Bloggs <mail@example.com>
- Jane Doe <mail+1@example.com>
- Jasper Jesperson <mail+2@example.com>
```
#### Simple array format with names
```
to: [mail@exmaple.com, Joe Bloggs]
```
#### Formatted email address with names
```
to:
email: mail@example.com
name: Joe Bloggs
```
or inline:
```
to: {email: 'mail@example.com', name: 'Joe Bloggs'}
```
#### Multiple email addresses (with and without names)
```
to:
- [mail@example.com, Joe Bloggs]
- [mail+2@example.com, Jane Doe]
```
```
to:
-
email: mail@example.com
name: Joe Bloggs
-
email: mail+2@example.com
name: Jane Doe
```
or inline:
```
to:
- {email: 'mail@example.com', name: 'Joe Bloggs'}
- {email: 'mail+2@example.com', name: 'Jane Doe'}
```
## Multi-part MIME messages
Apart from a simple string, an email body may contain different MIME parts (e.g. HTML body with plain text fallback):
```
body:
-
content_type: 'text/html'
body: "{% include 'forms/default/data.html.twig' %}"
-
content_type: 'text/plain'
body: "{% include 'forms/default/data.txt.twig' %}"
```
# Troubleshooting
## Emails are not sent
#### Debugging
The first step in determining why emails are not sent is to enable debugging. This can be done via the `user/config/email.yaml` file or via the plugin settings in the admin. Just enable this and then try sending an email again. Then inspect the `logs/email.log` file for potential problems.
#### ISP Port 25 blocking
By default, when sending via PHP or Sendmail the machine running the webserver will attempt to send mail using the SMTP protocol. This uses port `25` which is often blocked by ISPs to protected against spamming. You can determine if this port is blocked by running this command in your temrinal (mac/linux only):
```
(echo >/dev/tcp/localhost/25) &>/dev/null && echo "TCP port 25 opened" || echo "TCP port 25 closed"
```
If it's blocked there are ways to configure relays to different ports, but the simplest solution is to use SMTP for mail sending.
#### Exceptions
If you get an exception when sending email but you cannot see what the error is, you need to enable more verbose exception messages. In the `user/config/system.yaml` file ensure your have the following configuration:
```
errors:
display: 1
log: true
```
## Configuration Issues
As explained above in the Configuration section, if you're using the default settings, set the Plugin configuration to use a SMTP server. It can be [Gmail](https://www.digitalocean.com/community/tutorials/how-to-use-google-s-smtp-server) or another SMTP server you have at your disposal.
This is the first thing to check. The reason is that PHP Mail, the default system used by the Plugin, is not 100% reliable and emails might not arrive.

View File

@@ -0,0 +1,172 @@
name: Email
slug: email
type: plugin
version: 4.2.0
testing: false
description: Enables the emailing system for Grav
icon: envelope
author:
name: Team Grav
email: devs@getgrav.org
url: http://getgrav.org
keywords: plugin, email, sender
homepage: https://github.com/getgrav/grav-plugin-email
bugs: https://github.com/getgrav/grav-plugin-email/issues
license: MIT
dependencies:
- { name: grav, version: '>=1.7.41' }
- { name: form, version: '>=7.0.0' }
form:
validation: loose
fields:
enabled:
type: hidden
label: PLUGIN_ADMIN.PLUGIN_STATUS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
mailer.engine:
type: select
label: PLUGIN_EMAIL.MAIL_ENGINE
size: medium
description: PLUGIN_EMAIL.MAIL_ENGINE_DESC
data-options@: '\Grav\Plugin\EmailPlugin::getEngines'
smtp_config:
type: section
title: PLUGIN_EMAIL.SMTP_CONFIGURATION
underline: true
mailer.smtp.server:
type: text
size: medium
label: PLUGIN_EMAIL.SMTP_SERVER
placeholder: PLUGIN_EMAIL.SMTP_SERVER_PLACEHOLDER
mailer.smtp.port:
type: text
size: small
label: PLUGIN_EMAIL.SMTP_PORT
placeholder: PLUGIN_EMAIL.SMTP_PORT_PLACEHOLDER
validate:
type: number
min: 1
max: 65535
mailer.smtp.encryption:
type: select
size: medium
label: PLUGIN_EMAIL.SMTP_ENCRYPTION
options:
none: PLUGIN_EMAIL.SMTP_ENCRYPTION_NONE
ssl: SSL
tls: TLS
mailer.smtp.user:
type: text
size: medium
autocomplete: 'off'
label: PLUGIN_EMAIL.SMTP_LOGIN_NAME
mailer.smtp.password:
type: password
size: medium
autocomplete: 'new-password'
label: PLUGIN_EMAIL.SMTP_PASSWORD
sendmail_config:
type: section
title: PLUGIN_EMAIL.SENDMAIL_CONFIGURATION
underline: true
mailer.sendmail.bin:
type: text
size: medium
label: PLUGIN_EMAIL.PATH_TO_SENDMAIL
placeholder: "/usr/sbin/sendmail"
email_Defaults:
type: section
title: PLUGIN_EMAIL.EMAIL_DEFAULTS
underline: true
content_type:
type: select
label: PLUGIN_EMAIL.CONTENT_TYPE
size: medium
default: 'text/html'
options:
'text/plain': PLUGIN_EMAIL.CONTENT_TYPE_PLAIN_TEXT
'text/html': HTML
from:
type: text
size: large
label: PLUGIN_EMAIL.EMAIL_FORM
placeholder: PLUGIN_EMAIL.EMAIL_FORM_PLACEHOLDER
help: PLUGIN_EMAIL.EMAIL_FORMAT
validate:
required: true
to:
type: text
size: large
label: PLUGIN_EMAIL.EMAIL_TO
placeholder: PLUGIN_EMAIL.EMAIL_TO_PLACEHOLDER
help: PLUGIN_EMAIL.EMAIL_FORMAT
validate:
required: true
cc:
type: text
size: large
label: PLUGIN_EMAIL.EMAIL_CC
placeholder: PLUGIN_EMAIL.EMAIL_CC_PLACEHOLDER
help: PLUGIN_EMAIL.EMAIL_FORMAT
bcc:
type: text
size: large
label: PLUGIN_EMAIL.EMAIL_BCC
placeholder: PLUGIN_EMAIL.EMAIL_BCC_PLACEHOLDER
help: PLUGIN_EMAIL.EMAIL_FORMAT
reply_to:
type: text
size: large
label: PLUGIN_EMAIL.EMAIL_REPLY_TO
placeholder: PLUGIN_EMAIL.EMAIL_REPLY_TO_PLACEHOLDER
help: PLUGIN_EMAIL.EMAIL_FORMAT
body:
type: textarea
size: large
rows: 10
label: PLUGIN_EMAIL.EMAIL_BODY
placeholder: PLUGIN_EMAIL.EMAIL_BODY_PLACEHOLDER
advanced_section:
type: section
title: PLUGIN_EMAIL.ADVANCED
underline: true
debug:
type: toggle
label: PLUGIN_EMAIL.DEBUG
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool

View File

@@ -0,0 +1,537 @@
<?php
namespace Grav\Plugin\Email;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Common\Language\Language;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Twig\Twig;
use Grav\Framework\Form\Interfaces\FormInterface;
use \Monolog\Logger;
use \Monolog\Handler\StreamHandler;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Address;
class Email
{
/** @var Mailer */
protected $mailer;
/** @var TransportInterface */
protected $transport;
protected $log;
protected $message;
protected $debug;
public function __construct()
{
$this->initMailer();
$this->initLog();
}
/**
* Returns true if emails have been enabled in the system.
*
* @return bool
*/
public static function enabled(): bool
{
return Grav::instance()['config']->get('plugins.email.mailer.engine') !== 'none';
}
/**
* Returns true if debugging on emails has been enabled.
*
* @return bool
*/
public static function debug(): bool
{
return Grav::instance()['config']->get('plugins.email.debug') == 'true';
}
/**
* Creates an email message.
*
* @param string|null $subject
* @param string|null $body
* @param string|null $contentType
* @param string|null $charset @deprecated
* @return Message
*/
public function message(?string $subject = null, ?string $body = null, ?string $contentType = null, ?string $charset = null): Message
{
$message = new Message();
$message->subject($subject);
if ($contentType === 'text/html') {
$message->html($body);
} else {
$message->text($body);
}
return $message;
}
/**
* Send email.
*
* @param Message $message
* @param Envelope|null $envelope
* @return int
*/
public function send(Message $message, ?Envelope $envelope = null): int
{
try {
$sent_msg = $this->transport->send($message->getEmail(), $envelope);
$status = 1;
$this->message = '✅';
$this->debug = $sent_msg->getDebug();
} catch (TransportExceptionInterface $e) {
$status = 0;
$this->message = '🛑 ' . $e->getMessage();
$this->debug = $e->getDebug();
}
if ($this->debug()) {
$log_msg = "Email sent to %s at %s -> %s\n%s";
$to = $this->jsonifyRecipients($message->getEmail()->getTo());
$message = sprintf($log_msg, $to, date('Y-m-d H:i:s'), $this->message, $this->debug);
$this->log->info($message);
}
return $status;
}
/**
* Build e-mail message.
*
* @param array $params
* @param array $vars
* @return Message
*/
public function buildMessage(array $params, array $vars = []): Message
{
/** @var Twig $twig */
$twig = Grav::instance()['twig'];
$twig->init();
/** @var Config $config */
$config = Grav::instance()['config'];
/** @var Language $language */
$language = Grav::instance()['language'];
// Create message object.
$message = new Message();
$headers = $message->getEmail()->getHeaders();
$email = $message->getEmail();
// Extend parameters with defaults.
$defaults = [
'bcc' => $config->get('plugins.email.bcc', []),
'bcc_name' => $config->get('plugins.email.bcc_name'),
'body' => $config->get('plugins.email.body', '{% include "forms/data.html.twig" %}'),
'cc' => $config->get('plugins.email.cc', []),
'cc_name' => $config->get('plugins.email.cc_name'),
'charset' => $config->get('plugins.email.charset', 'utf-8'),
'from' => $config->get('plugins.email.from'),
'from_name' => $config->get('plugins.email.from_name'),
'content_type' => $config->get('plugins.email.content_type', 'text/html'),
'reply_to' => $config->get('plugins.email.reply_to', []),
'reply_to_name' => $config->get('plugins.email.reply_to_name'),
'subject' => !empty($vars['form']) && $vars['form'] instanceof FormInterface ? $vars['form']->page()->title() : null,
'to' => $config->get('plugins.email.to'),
'to_name' => $config->get('plugins.email.to_name'),
'process_markdown' => false,
'template' => false,
'message' => $message
];
foreach ($defaults as $key => $value) {
if (!key_exists($key, $params)) {
$params[$key] = $value;
}
}
if (!$params['to']) {
throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_TO_ADDRESS'));
}
if (!$params['from']) {
throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_FROM_ADDRESS'));
}
// make email configuration available to templates
$vars += [
'email' => $params,
];
$params = $this->processParams($params, $vars);
// Process parameters.
foreach ($params as $key => $value) {
switch ($key) {
case 'body':
if (is_string($value)) {
$this->processBody($message, $params, $vars, $twig, $value);
} elseif (is_array($value)) {
foreach ($value as $body_part) {
$params_part = $params;
if (isset($body_part['content_type'])) {
$params_part['content_type'] = $body_part['content_type'];
}
if (isset($body_part['template'])) {
$params_part['template'] = $body_part['template'];
}
if (isset($body_part['body'])) {
$this->processBody($message, $params_part, $vars, $twig, $body_part['body']);
}
}
}
break;
case 'subject':
if ($value) {
$message->subject($language->translate($value));
}
break;
case 'to':
case 'from':
case 'cc':
case 'bcc':
case 'reply_to':
if ($recipients = $this->processRecipients($key, $params)) {
$key = $key === 'reply_to' ? 'replyTo' : $key;
$email->$key(...$recipients);
}
break;
case 'tags':
foreach ((array) $value as $tag) {
if (is_string($tag)) {
$headers->add(new TagHeader($tag));
}
}
break;
case 'metadata':
foreach ((array) $value as $k => $v) {
if (is_string($k) && is_string($v)) {
$headers->add(new MetadataHeader($k, $v));
}
}
break;
}
}
return $message;
}
/**
* @param string $type
* @param array $params
* @return array
*/
protected function processRecipients(string $type, array $params): array
{
if (array_key_exists($type, $params) && $params[$type] === null) {
return [];
}
$recipients = $params[$type] ?? Grav::instance()['config']->get('plugins.email.'.$type) ?? [];
$list = [];
if (!empty($recipients)) {
if (is_array($recipients)) {
if (Utils::isAssoc($recipients) || (count($recipients) ===2 && $this->isValidEmail($recipients[0]) && !$this->isValidEmail($recipients[1]))) {
$list[] = $this->createAddress($recipients);
} else {
foreach ($recipients as $recipient) {
$list[] = $this->createAddress($recipient);
}
}
} else {
if (is_string($recipients) && Utils::contains($recipients, ',')) {
$recipients = array_map('trim', explode(',', $recipients));
foreach ($recipients as $recipient) {
$list[] = $this->createAddress($recipient);
}
} else {
if (!Utils::contains($recipients, ['<','>']) && (isset($params[$type."_name"]))) {
$recipients = [$recipients, $params[$type."_name"]];
}
$list[] = $this->createAddress($recipients);
}
}
}
return $list;
}
/**
* @param $data
* @return Address
*/
protected function createAddress($data): Address
{
if (is_string($data)) {
preg_match('/^(.*)\<(.*)\>$/', $data, $matches);
if (isset($matches[2])) {
$email = trim($matches[2]);
$name = trim($matches[1]);
} else {
$email = $data;
$name = '';
}
} elseif (Utils::isAssoc($data)) {
$first_key = array_key_first($data);
if (filter_var($first_key, FILTER_VALIDATE_EMAIL)) {
$email = $first_key;
$name = $data[$first_key];
} else {
$email = $data['email'] ?? $data['mail'] ?? $data['address'] ?? '';
$name = $data['name'] ?? $data['fullname'] ?? '';
}
} else {
$email = $data[0] ?? '';
$name = $data[1] ?? '';
}
return new Address($email, $name);
}
/**
* @return null|Mailer
* @internal
*/
protected function initMailer(): ?Mailer
{
if (!$this->enabled()) {
return null;
}
if (!$this->mailer) {
$this->transport = $this->getTransport();
// Create the Mailer using your created Transport
$this->mailer = new Mailer($this->transport);
}
return $this->mailer;
}
/**
* @return void
* @throws \Exception
*/
protected function initLog()
{
$log_file = Grav::instance()['locator']->findResource('log://email.log', true, true);
$this->log = new Logger('email');
/** @var UniformResourceLocator $locator */
$this->log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));
}
/**
* @param array $params
* @param array $vars
* @return array
*/
protected function processParams(array $params, array $vars = []): array
{
$twig = Grav::instance()['twig'];
array_walk_recursive($params, function(&$value) use ($twig, $vars) {
if (is_string($value)) {
$value = $twig->processString($value, $vars);
}
});
return $params;
}
/**
* @param $message
* @param $params
* @param $vars
* @param $twig
* @param $body
* @return void
*/
protected function processBody($message, $params, $vars, $twig, $body)
{
if ($params['process_markdown'] && $params['content_type'] === 'text/html') {
$body = (new Parsedown())->text($body);
}
if ($params['template']) {
$body = $twig->processTemplate($params['template'], ['content' => $body] + $vars);
}
$content_type = !empty($params['content_type']) ? $twig->processString($params['content_type'], $vars) : null;
if ($content_type === 'text/html') {
$message->html($body);
} else {
$message->text($body);
}
}
/**
* @return TransportInterface
*/
protected static function getTransport(): Transport\TransportInterface
{
/** @var Config $config */
$config = Grav::instance()['config'];
$engine = $config->get('plugins.email.mailer.engine');
$dsn = 'null://default';
// Create the Transport and initialize it.
switch ($engine) {
case 'smtps':
case 'smtp':
$options = $config->get('plugins.email.mailer.smtp');
$dsn = $engine . '://';
$auth = '';
if (isset($options['encryption']) && $options['encryption'] === 'none') {
$options['options']['verify_peer'] = 0;
}
if (isset($options['user'])) {
$auth .= urlencode($options['user']);
}
if (isset($options['password'])) {
$auth .= ':'. urlencode($options['password']);
}
if (!empty($auth)) {
$dsn .= "$auth@";
}
if (isset($options['server'])) {
$dsn .= urlencode($options['server']);
}
if (isset($options['port'])) {
$dsn .= ":{$options['port']}";
}
if (isset($options['options'])) {
$dsn .= '?' . http_build_query($options['options']);
}
break;
case 'mail':
case 'native':
$dsn = 'native://default';
break;
case 'sendmail':
$dsn = 'sendmail://default';
$bin = $config->get('plugins.email.mailer.sendmail.bin');
if (isset($bin)) {
$dsn .= '?command=' . urlencode($bin);
}
break;
default:
$e = new Event(['engine' => $engine, ]);
Grav::instance()->fireEvent('onEmailTransportDsn', $e);
if (isset($e['dsn'])) {
$dsn = $e['dsn'];
}
break;
}
if ($dsn instanceof TransportInterface) {
$transport = $dsn;
} else {
$transport = Transport::fromDsn($dsn) ;
}
return $transport;
}
/**
* Get any message from the last send attempt
* @return string|null
*/
public function getLastSendMessage(): ?string
{
return $this->message;
}
/**
* Get any debug information from the last send attempt
* @return string|null
*/
public function getLastSendDebug(): ?string
{
return $this->debug;
}
/**
* @param array $recipients
* @return string
*/
protected function jsonifyRecipients(array $recipients): string
{
$json = [];
foreach ($recipients as $recipient) {
$json[] = str_replace('"', "", $recipient->toString());
}
return json_encode($json);
}
protected function isValidEmail($email): bool
{
return is_string($email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* @return void
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
*/
public static function flushQueue() {}
/**
* @return void
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
*/
public static function clearQueueFailures() {}
/**
* Creates an attachment.
*
* @param string $data
* @param string $filename
* @param string $contentType
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
* @return void
*/
public function attachment($data = null, $filename = null, $contentType = null) {}
/**
* Creates an embedded attachment.
*
* @param string $data
* @param string $filename
* @param string $contentType
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
* @return void
*/
public function embedded($data = null, $filename = null, $contentType = null) {}
/**
* Creates an image attachment.
*
* @param string $data
* @param string $filename
* @param string $contentType
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
* @return void
*/
public function image($data = null, $filename = null, $contentType = null) {}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Grav\Plugin\Email;
use Symfony\Component\Mime\Email as SymfonyEmail;
class Message
{
/** @var SymfonyEmail */
protected $email;
public function __construct() {
$this->email = new SymfonyEmail();
}
public function subject($subject): self
{
$this->email->subject($subject);
return $this;
}
public function setSubject($subject): self
{
$this->subject($subject);
return $this;
}
public function to($to): self
{
$this->email->to($to);
return $this;
}
public function from($from): self
{
$this->email->from($from);
return $this;
}
public function cc($cc): self
{
$this->email->cc($cc);
return $this;
}
public function bcc($bcc): self
{
$this->email->bcc($bcc);
return $this;
}
public function replyTo($reply_to): self
{
$this->email->replyTo($reply_to);
return $this;
}
public function text($text): self
{
$this->email->text($text);
return $this;
}
public function html($html): self
{
$this->email->html($html);
return $this;
}
public function attachFromPath($path): self
{
$this->email->attachFromPath($path);
return $this;
}
public function embedFromPath($path): self
{
$this->email->embedFromPath($path);
return $this;
}
public function reply_to($reply_to): self
{
$this->replyTo($reply_to);
return $this;
}
public function setFrom($from): self
{
$this->from($from);
return $this;
}
public function setTo($to): self
{
$this->to($to);
return $this;
}
public function getEmail(): SymfonyEmail
{
return $this->email;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Grav\Plugin\Email;
use Grav\Common\Grav;
use Grav\Common\Twig\Twig;
use Grav\Common\Utils as GravUtils;
/**
* Class Utils
* @package Grav\Plugin\Email
*/
class Utils
{
/**
* Quick utility method to send an HTML email.
*
* @param array<int,mixed> $params
*
* @return bool True if the action was performed.
*/
public static function sendEmail(...$params)
{
if (is_array($params[0])) {
$params = array_shift($params);
} else {
$keys = ['subject', 'body', 'to', 'from', 'content_type'];
$params = GravUtils::arrayCombine($keys, $params);
}
//Initialize twig if not yet initialized
/** @var Twig $twig */
$twig = Grav::instance()['twig']->init();
/** @var Email $email */
$email = Grav::instance()['Email'];
if (empty($params['to']) || empty($params['subject']) || empty($params['body'])) {
return false;
}
$params['body'] = $twig->processTemplate('email/base.html.twig', ['content' => $params['body']]);
$message = $email->buildMessage($params);
return $email->send($message);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Grav\Plugin\Console;
use Grav\Common\Grav;
use Grav\Console\ConsoleCommand;
use Grav\Plugin\Email\Email;
use Symfony\Component\Console\Input\InputOption;
/**
* Class FlushQueueCommand
* @package Grav\Console\Cli\
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
*/
class ClearQueueFailuresCommand extends ConsoleCommand
{
/** @var array */
protected $options = [];
/**
* @return void
*/
protected function configure()
{
$this
->setName('clear-queue-failures')
->setAliases(['clearqueue'])
->setDescription('DEPRECATED: Clears any queue failures that have accumulated')
->setHelp('The <info>clear-queue-failures</info> command clears any queue failures that have accumulated');
}
/**
* @return int
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
*/
protected function serve()
{
return 0;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Grav\Plugin\Console;
use Grav\Common\Grav;
use Grav\Console\ConsoleCommand;
use Grav\Plugin\Email\Email;
use Symfony\Component\Console\Input\InputOption;
/**
* Class FlushQueueCommand
* @package Grav\Console\Cli\
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
*/
class FlushQueueCommand extends ConsoleCommand
{
/** @var array */
protected $options = [];
/**
* @return void
*/
protected function configure()
{
$this
->setName('flush-queue')
->setAliases(['flushqueue'])
->setDescription('DEPRECATED: Flushes the email queue of any pending emails')
->setHelp('The <info>flush-queue</info> command flushes the email queue of any pending emails');
}
/**
* @return int
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
*/
protected function serve()
{
return 0;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Grav\Plugin\Console;
use Grav\Common\Grav;
use Grav\Common\Data\Data;
use Grav\Console\ConsoleCommand;
use Grav\Plugin\Email\Email;
use Grav\Plugin\Email\Utils as EmailUtils;
use Symfony\Component\Console\Input\InputOption;
/**
* Class TestEmailCommand
* @package Grav\Console\Cli\
*/
class TestEmailCommand extends ConsoleCommand
{
/** @var array */
protected $options = [];
/**
* @return void
*/
protected function configure()
{
$this
->setName('test-email')
->setAliases(['testemail'])
->addOption(
'to',
't',
InputOption::VALUE_REQUIRED,
'An email address to send the email to'
)
->addOption(
'env',
'e',
InputOption::VALUE_OPTIONAL,
'The environment to trigger a specific configuration. For example: localhost, mysite.dev, www.mysite.com'
)
->addOption(
'subject',
's',
InputOption::VALUE_OPTIONAL,
'A subject for the email'
)
->addOption(
'body',
'b',
InputOption::VALUE_OPTIONAL,
'The body of the email'
)
->setDescription('Sends a test email using the plugin\'s configuration')
->setHelp('The <info>test-email</info> command sends a test email using the plugin\'s configuration');
}
/**
* @return int
*/
protected function serve()
{
// TODO: remove when requiring Grav 1.7+
if (method_exists($this, 'initializeGrav')) {
$this->initializeThemes();
}
$this->output->writeln('');
$this->output->writeln('<yellow>Current Configuration:</yellow>');
$this->output->writeln('');
$grav = Grav::instance();
$email_config = new Data($grav['config']->get('plugins.email'));
if ($email_config->get('mailer.smtp.password')) {
$password = $email_config->get('mailer.smtp.password');
$obfuscated_password = str_repeat('*', strlen($password) - 2) . substr($password, -2);
$email_config->set('mailer.smtp.password', $obfuscated_password);
}
dump($email_config);
$this->output->writeln('');
$to = $this->input->getOption('to') ?: $grav['config']->get('plugins.email.to');
$subject = $this->input->getOption('subject');
$body = $this->input->getOption('body');
if (!$subject) {
$subject = 'Testing Grav Email Plugin';
}
if (!$body) {
$configuration = print_r($email_config, true);
$body = $grav['language']->translate(['PLUGIN_EMAIL.TEST_EMAIL_BODY', $configuration]);
}
$sent = EmailUtils::sendEmail(['subject'=>$subject, 'body'=>$body, 'to'=>$to]);
if ($sent) {
$this->output->writeln("<green>Message sent successfully!</green>");
} else {
$this->output->writeln("<red>Problem sending email...</red>");
}
return 0;
}
}

View File

@@ -0,0 +1,44 @@
{
"name": "getgrav/grav-plugin-email",
"type": "grav-plugin",
"description": "Email plugin for Grav CMS",
"keywords": ["email", "plugin", "sender"],
"homepage": "https://github.com/getgrav/grav-plugin-email",
"license": "MIT",
"authors": [
{
"name": "Team Grav",
"email": "devs@getgrav.org",
"homepage": "https://getgrav.org",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/getgrav/grav-plugin-email/issues",
"irc": "https://chat.getgrav.org",
"forum": "https://getgrav.org/forum",
"docs": "https://github.com/getgrav/grav-plugin-email/blob/master/README.md"
},
"require": {
"php": ">=7.3.6",
"symfony/mailer": "^5.4",
"symfony/messenger": "^5.4"
},
"replace": {
"symfony/polyfill-iconv": "*",
"symfony/polyfill-mbstring": "*",
"symfony/polyfill-php72": "*"
},
"autoload": {
"psr-4": {
"Grav\\Plugin\\Email\\": "classes/",
"Grav\\Plugin\\Console\\": "cli/"
},
"classmap": ["email.php"]
},
"config": {
"platform": {
"php": "7.3.6"
}
}
}

1395
config/www/user/plugins/email/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
<?php
namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Grav\Common\Data\Data;
use Grav\Common\Data\ValidationException;
use Grav\Common\Grav;
use Grav\Common\Plugin;
use Grav\Common\Utils;
use Grav\Plugin\Email\Email;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Mailer\Exception\TransportException;
class EmailPlugin extends Plugin
{
/**
* @var Email
*/
protected $email;
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
'onFormProcessed' => ['onFormProcessed', 0],
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
'onSchedulerInitialized' => ['onSchedulerInitialized', 0],
'onAdminSave' => ['onAdminSave', 0],
];
}
/**
* @return ClassLoader
*/
public function autoload(): ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
/**
* Initialize emailing.
*/
public function onPluginsInitialized()
{
$this->email = new Email();
if ($this->email::enabled()) {
$this->grav['Email'] = $this->email;
}
}
/**
* Add twig paths to plugin templates.
*/
public function onTwigTemplatePaths()
{
$twig = $this->grav['twig'];
$twig->twig_paths[] = __DIR__ . '/templates';
}
/**
* Force compile during save if admin plugin save
*
* @param Event $event
*/
public function onAdminSave(Event $event)
{
/** @var Data $obj */
$obj = $event['object'];
if ($obj instanceof Data && $obj->blueprints()->getFilename() === 'email/blueprints') {
$current_pw = $this->grav['config']->get('plugins.email.mailer.smtp.password');
$new_pw = $obj->get('mailer.smtp.password');
if (!empty($current_pw) && empty($new_pw)) {
$obj->set('mailer.smtp.password', $current_pw);
}
}
}
/**
* Send email when processing the form data.
*
* @param Event $event
*/
public function onFormProcessed(Event $event)
{
$form = $event['form'];
$action = $event['action'];
$params = $event['params'];
if (!$this->email->enabled()) {
return;
}
switch ($action) {
case 'email':
// Prepare Twig variables
$vars = array(
'form' => $form,
'page' => $this->grav['page']
);
// Copy files now, we need those.
// TODO: needs an update
$form->legacyUploads();
$form->copyFiles();
$this->grav->fireEvent('onEmailSend', new Event(['params' => &$params, 'vars' => &$vars]));
if (Utils::isAssoc($params)) {
$this->sendFormEmail($form, $params, $vars, $event);
} else {
foreach ($params as $email) {
$this->sendFormEmail($form, $email, $vars, $event);
}
}
break;
}
}
protected function sendFormEmail($form, $params, $vars, $event)
{
// Build message
$message = $this->email->buildMessage($params, $vars);
$locator = $this->grav['locator'];
if (isset($params['attachments'])) {
$filesToAttach = (array)$params['attachments'];
if ($filesToAttach) foreach ($filesToAttach as $fileToAttach) {
$filesValues = $form->value($fileToAttach);
if ($filesValues) foreach($filesValues as $fileValues) {
if (isset($fileValues['file'])) {
$filename = $fileValues['file'];
} else {
$filename = $fileValues['path'];
}
$filename = $locator->findResource($filename, true, true);
try {
$message->attachFromPath($filename);
} catch (\Exception $e) {
// Log any issues
$this->grav['log']->error($e->getMessage());
}
}
}
}
//fire event to apply optional signers
$this->grav->fireEvent('onEmailMessage', new Event(['message' => $message, 'params' => $params, 'form' => $form]));
// Send e-mail
$status = $this->email->send($message);
if ($status < 1) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => $this->email->getLastSendMessage(),
]));
$event->stopPropagation();
return;
}
//fire event after eMail was sent
$this->grav->fireEvent('onEmailSent', new Event(['message' => $message, 'params' => $params, 'form' => $form]));
}
/**
* Used for dynamic blueprint field
*
* @return array
*/
public static function getEngines(): array
{
$engines = (object) [
'sendmail' => 'Sendmail',
'smtp' => 'SMTP',
'smtps' => 'SMTPS',
'native' => 'Native',
'none' => 'PLUGIN_ADMIN.DISABLED',
];
Grav::instance()->fireEvent('onEmailEngines', new Event(['engines' => $engines]));
return (array) $engines;
}
/**
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
*/
public function onSchedulerInitialized(Event $e)
{
}
}

View File

@@ -0,0 +1,15 @@
enabled: true
from:
to:
mailer:
engine: sendmail
smtp:
server: localhost
port: 25
encryption: none
user:
password:
sendmail:
bin: '/usr/sbin/sendmail -bs'
content_type: text/html
debug: false

View File

@@ -0,0 +1,218 @@
en:
PLUGIN_EMAIL:
MAIL_ENGINE: "Mail Engine"
MAIL_ENGINE_DISABLED: "Disabled"
MAIL_ENGINE_DESC: "NOTE: If you select an engine provided by another plugin, you should configure options in that plugin."
CONTENT_TYPE: "Content type"
CONTENT_TYPE_PLAIN_TEXT: "Plain text"
CHARSET: "Charset"
CHARSET_PLACEHOLDER: "Defaults to UTF-8"
EMAIL_FORM: "From address"
EMAIL_FORM_PLACEHOLDER: "Default email from address"
EMAIL_FROM_NAME: "From name"
EMAIL_FROM_NAME_PLACEHOLDER: "Default email from name"
EMAIL_TO: "To address"
EMAIL_TO_PLACEHOLDER: "Default email to address"
EMAIL_TO_NAME: "To name"
EMAIL_TO_NAME_PLACEHOLDER: "Default email to name"
EMAIL_CC: "CC address"
EMAIL_CC_PLACEHOLDER: "Default email CC address"
EMAIL_CC_NAME: "CC name"
EMAIL_CC_NAME_PLACEHOLDER: "Default email CC name"
EMAIL_BCC: "BCC address"
EMAIL_BCC_PLACEHOLDER: "Default email BCC address"
EMAIL_REPLY_TO: "Reply-to address"
EMAIL_REPLY_TO_PLACEHOLDER: "Default email reply-to address"
EMAIL_REPLY_TO_NAME: "Reply-to name"
EMAIL_REPLY_TO_NAME_PLACEHOLDER: "Default email reply-to name"
EMAIL_BODY: "Body"
EMAIL_BODY_PLACEHOLDER: "Defaults to a table of all form fields"
SMTP_SERVER: "SMTP server"
SMTP_SERVER_PLACEHOLDER: "e.g. smtp.google.com"
SMTP_PORT: "SMTP port"
SMTP_PORT_PLACEHOLDER: "Defaults to 25 (plaintext) / 587 (encrypted)"
SMTP_ENCRYPTION: "SMTP encryption"
SMTP_ENCRYPTION_NONE: "None"
SMTP_LOGIN_NAME: "SMTP login name"
SMTP_PASSWORD: "SMTP password"
SMTP_AUTH_MODE: "SMTP auth mode"
PATH_TO_SENDMAIL: "Path to sendmail binary"
DEBUG: "Debug"
EMAIL_NOT_CONFIGURED: "Email not configured"
PLEASE_CONFIGURE_A_TO_ADDRESS: "Please configure a 'to' address in the Email Plugin settings, or in the form"
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Please configure a 'from' address in the Email Plugin settings, or in the form"
TEST_EMAIL_BODY: "<h1>Testing Email</h1><p>This test email has been sent based on the following configuration:</p> <p><pre>%1$s</pre></p>"
EMAIL_DEFAULTS: "Email Defaults"
SMTP_CONFIGURATION: "SMTP/S Configuration"
SENDMAIL_CONFIGURATION: "Sendmail Configuration"
ADVANCED: "Advanced"
EMAIL_FOOTER: "GetGrav.org"
QUEUE_TITLE: "Email Queue"
QUEUE_DESC: "When you enable the email queue, email is not <i>sent immediately</i>, rather the email is sent to the queue, and then the Grav <strong>scheduler will flush the queue</strong> and actually send the email based on the configured frequency. This ensures Grav is not waiting around for email connections to complete."
QUEUE_ENABLED: "Enabled"
QUEUE_FLUSH_FREQUENCY: "Flush Frequency"
QUEUE_FLUSH_FREQUENCY_HELP: "Use 'cron' format"
QUEUE_FLUSH_MSG_LIMIT: "Messages per flush"
QUEUE_FLUSH_MSG_LIMIT_APPEND: "Messages"
QUEUE_FLUSH_TIME_LIMIT: "Flush time limit"
QUEUE_FLUSH_TIME_LIMIT_APPEND: "Seconds"
EMAIL_FORMAT: "Use the `addr` format: `email@address.org` or `name-addr` format: `Your Name <email@address.org>`. Comma separated for multiple addresses"
da:
PLUGIN_EMAIL:
PLEASE_CONFIGURE_A_TO_ADDRESS: "Konfigurere venligst en 'til' email adresse i Email Plugin indstillingerne eller her i formularen"
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Konfigurere venligst en 'fra' email adresse i Email Plugin indstillingerne eller her i formularen"
de:
PLUGIN_EMAIL:
EMAIL_NOT_CONFIGURED: "E-Mail ist nicht konfiguriert"
PLEASE_CONFIGURE_A_TO_ADDRESS: "Bitte konfigurieren sie eine 'An' ('to') Adresse in den Email-Plugin-Einstellungen oder im Formular."
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Bitte konfigurieren sie eine 'Von' ('from') Adresse in den Email-Plugin-Einstellungen oder im Formular."
es:
PLUGIN_EMAIL:
PLEASE_CONFIGURE_A_TO_ADDRESS: "Por favor configura una dirección de 'remitente' en la configuración del Plugin de Email o en el formulario"
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Por favor configura una dirección de 'destinatario' en la configuración del Plugin de Email o en el formulario"
fr:
PLUGIN_EMAIL:
MAIL_ENGINE: "Moteur de messagerie"
MAIL_ENGINE_DISABLED: "Désactivé"
MAIL_ENGINE_DESC: "NOTE : Si vous sélectionnez un moteur fourni par un autre plugin, vous devez configurer les options dans ce même plugin."
CONTENT_TYPE: "Type de contenu"
CONTENT_TYPE_PLAIN_TEXT: "Texte brut"
CHARSET: "Jeu de caractères"
CHARSET_PLACEHOLDER: "Defaults to UTF-8"
EMAIL_FORM: "Adresse de lexpéditeur"
EMAIL_FORM_PLACEHOLDER: "Adresse e-mail par défaut de lexpéditeur"
EMAIL_FROM_NAME: "Nom de lexpéditeur"
EMAIL_FROM_NAME_PLACEHOLDER: "Nom par défaut de lexpéditeur"
EMAIL_TO: "Adresse du destinataire"
EMAIL_TO_PLACEHOLDER: "Adresse e-mail par défaut du destinataire"
EMAIL_TO_NAME: "Nom du destinataire"
EMAIL_TO_NAME_PLACEHOLDER: "Nom par défaut du destinataire"
EMAIL_CC: "Adresse en CC"
EMAIL_CC_PLACEHOLDER: "Adresse e-mail par défaut en CC"
EMAIL_CC_NAME: "Nom en CC"
EMAIL_CC_NAME_PLACEHOLDER: "Nom par défaut en CC"
EMAIL_BCC: "Adresse en BCC"
EMAIL_BCC_PLACEHOLDER: "Adresse e-mail par défaut en BCC"
EMAIL_REPLY_TO: "Adresse de réponse"
EMAIL_REPLY_TO_PLACEHOLDER: "Adresse e-mail par défaut pour la réponse"
EMAIL_REPLY_TO_NAME: "Nom pour la réponse"
EMAIL_REPLY_TO_NAME_PLACEHOLDER: "Nom par défaut pour la réponse"
EMAIL_BODY: "Corps"
EMAIL_BODY_PLACEHOLDER: "Table de tous les champs de formulaire par défaut"
SMTP_SERVER: "Serveur SMTP"
SMTP_SERVER_PLACEHOLDER: "ex. smtp.google.com"
SMTP_PORT: "Port SMTP"
SMTP_PORT_PLACEHOLDER: "25 (texte brut) / 587 (encrypté) par défaut"
SMTP_ENCRYPTION: "cryptage SMTP"
SMTP_ENCRYPTION_NONE: "Aucun"
SMTP_LOGIN_NAME: "Nom d'utilisateur SMTP"
SMTP_PASSWORD: "Mot de passe SMTP"
SMTP_AUTH_MODE: "Mode d'authentification SMTP"
PATH_TO_SENDMAIL: "Chemin vers sendmail"
DEBUG: "Débogage"
EMAIL_NOT_CONFIGURED: "Le-mail nest pas configuré"
PLEASE_CONFIGURE_A_TO_ADDRESS: "Veuillez configurer une adresse de 'destinataire' dans les paramètres du plugin de-mail ou dans le formulaire."
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Veuillez configurer une adresse 'dexpéditeur' dans les paramètres du plugin de-mail ou dans le formulaire."
TEST_EMAIL_BODY: "<h1>E-mail de test</h1><p>Cet e-mail de test est basé sur la configuration suivante :</p> <p><pre>%1$s</pre></p>"
EMAIL_DEFAULTS: "E-mail par défaut"
SMTP_CONFIGURATION: "Configuration SMTP"
SENDMAIL_CONFIGURATION: "Configuration Sendmail"
ADVANCED: "Avancée"
EMAIL_FOOTER: "GetGrav.org"
QUEUE_TITLE: "Fil dattente de-mails"
QUEUE_DESC: "Lorsque vous activez la file dattente des e-mails, ils ne sont pas <i>envoyés immédiatement</i>. Au contraire, ils sont envoyés dans la file dattente, puis le planificateur Grav <strong>videra la file dattente</strong> et enverra les e-mails en fonction de la fréquence configurée. Cela permet de sassurer que Grav nattend pas la fin des connexions pour les envoyer"
QUEUE_ENABLED: "Activé"
QUEUE_FLUSH_FREQUENCY: "Fréquence de Flush"
QUEUE_FLUSH_FREQUENCY_HELP: "Utiliser le format 'cron'"
QUEUE_FLUSH_MSG_LIMIT: "Messages par Flush"
QUEUE_FLUSH_MSG_LIMIT_APPEND: "Messages"
QUEUE_FLUSH_TIME_LIMIT: "Délai de Flush"
QUEUE_FLUSH_TIME_LIMIT_APPEND: "Secondes"
EMAIL_FORMAT: "Utilisez le format `addr` : `email@adresse.org` ou le format `name-addr` : `Votre nom <email@adresse.org>`. Séparer par des virgules pour plusieurs adresses."
hr:
PLUGIN_EMAIL:
EMAIL_NOT_CONFIGURED: "Email nije konfiguriran"
PLEASE_CONFIGURE_A_TO_ADDRESS: "Konfigurirajte 'za' ('to') adresu u postavkama Email dodatka ili u obrascu"
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Konfigurirajte 'od' ('from') adresu u postavkama Email dodatka ili u obrascu"
it:
PLUGIN_EMAIL:
PLEASE_CONFIGURE_A_TO_ADDRESS: "Per favore, configura l'indirizzo di destinazione ('to') nella configurazione del Plugin Email, oppure direttamente nella form."
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Per favore, configura l'indirizzo di provenienza ('from') nella configurazione del Plugin Email, oppure direttamente nella form"
ro:
PLUGIN_EMAIL:
EMAIL_NOT_CONFIGURED: "Adresa de email nu este configurată"
PLEASE_CONFIGURE_A_TO_ADDRESS: "Vă rugam setați o adresă 'către' în setările modulului Email sau în formular"
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Vă rugam setați o adresă 'de la' în setările modulului Email sau în formular"
ru:
PLUGIN_EMAIL:
MAIL_ENGINE: "Почтовая система"
MAIL_ENGINE_DISABLED: "Отключена"
CONTENT_TYPE: "Тип контента"
CONTENT_TYPE_PLAIN_TEXT: "Простой текст"
CHARSET: "Кодировка"
CHARSET_PLACEHOLDER: "По умолчанию UTF-8"
EMAIL_DEFAULTS: "Email настройки по умолчанию"
EMAIL_FORM: "Почта отправителя"
EMAIL_FORM_PLACEHOLDER: "Email адрес отправителя по умолчанию"
EMAIL_FROM_NAME: "Имя почты отправителя"
EMAIL_FROM_NAME_PLACEHOLDER: "Email имя отправителя по умолчанию"
EMAIL_TO: "Почта получателя"
EMAIL_TO_PLACEHOLDER: "Email адрес получателя по умолчанию"
EMAIL_TO_NAME: "Имя почты получателя"
EMAIL_TO_NAME_PLACEHOLDER: "Email имя получателя по умолчанию"
EMAIL_CC: "Почта CC"
EMAIL_CC_PLACEHOLDER: "Email CC адрес по умолчанию"
EMAIL_CC_NAME: "Имя почты CC"
EMAIL_CC_NAME_PLACEHOLDER: "Email CC имя по умолчанию"
EMAIL_BCC: "Почта BCC"
EMAIL_BCC_PLACEHOLDER: "Email BCC адрес по умолчанию"
EMAIL_REPLY_TO: "Почта для ответов"
EMAIL_REPLY_TO_PLACEHOLDER: "Email для ответов адрес по умолчанию"
EMAIL_REPLY_TO_NAME: "Имя почты для ответов"
EMAIL_REPLY_TO_NAME_PLACEHOLDER: "Email для ответов имя по умолчанию"
EMAIL_BODY: "Тело сообщения"
EMAIL_BODY_PLACEHOLDER: "По умолчанию используется таблица всех полей формы"
SMTP_CONFIGURATION: "Конфигурация SMTP"
SMTP_SERVER: "SMTP сервер"
SMTP_SERVER_PLACEHOLDER: "e.g. smtp.google.com"
SMTP_PORT: "SMTP порт"
SMTP_PORT_PLACEHOLDER: "По умолчанию 25 (plaintext) / 587 (encrypted)"
SMTP_ENCRYPTION: "SMTP шифрование"
SMTP_ENCRYPTION_NONE: "Нет"
SMTP_LOGIN_NAME: "SMTP логин"
SMTP_PASSWORD: "SMTP пароль"
SENDMAIL_CONFIGURATION: "Конфигурация Sendmail"
PATH_TO_SENDMAIL: "Путь к sendmail"
QUEUE_TITLE: "Очередь Email"
QUEUE_DESC: "Когда вы включаете очередь email, электронная почта не <i>отправляется немедленно</i>, а отправляется в очередь, затем планировщик Grav <strong>обрабатывает очередь</strong> и на основе настроенной частоты фактически отправляет электронную почту. Это гарантирует, что Grav не ждет завершения подключения к электронной почте."
QUEUE_ENABLED: "Включено"
QUEUE_FLUSH_FREQUENCY: "Частота обработки"
QUEUE_FLUSH_FREQUENCY_HELP: "Использовать формат 'cron'"
QUEUE_FLUSH_MSG_LIMIT: "Количество сообщений на задачу"
QUEUE_FLUSH_MSG_LIMIT_APPEND: "Сообщений"
QUEUE_FLUSH_TIME_LIMIT: "Ограничение времени обработки"
QUEUE_FLUSH_TIME_LIMIT_APPEND: "Секунд"
ADVANCED: "Расширенные"
DEBUG: "Отладка"
EMAIL_NOT_CONFIGURED: "Электронная почта не настроена"
PLEASE_CONFIGURE_A_TO_ADDRESS: "Пожалуйста настройте адрес получателя ('to') в настройках плагина Email Plugin, или на форме"
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Пожалуйста настройте адрес отправителя ('from') в настройках плагина Email Plugin, или на форме"
TEST_EMAIL_BODY: "<h1>Тестирование электронной почты</h1><p>Это тестовое письмо отправлено на основе следующей конфигурации:</p> <p><pre>%1$s</pre></p>"
EMAIL_FOOTER: "GetGrav.org"
uk:
PLUGIN_EMAIL:
EMAIL_NOT_CONFIGURED: "Електронна пошта не налаштована"
PLEASE_CONFIGURE_A_TO_ADDRESS: "Будь ласка налаштуйте адресу одержувача ('to') в налаштуваннях плагіна Email Plugin, або на формі"
PLEASE_CONFIGURE_A_FROM_ADDRESS: "Будь ласка налаштуйте адресу відправника ('from') в налаштуваннях плагіна Email Plugin, або на формі"
TEST_EMAIL_BODY: "<h1>Тестування електронної пошти</h1><p>Це тестовий лист відправлено на основі такої конфігурації:</p> <p><pre>%1$s</pre></p>"
EMAIL_FOOTER: "GetGrav.org"

View File

@@ -0,0 +1,214 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<style>
/* -------------------------------------
GLOBAL
------------------------------------- */
* {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
font-size: 100%;
line-height: 1.6;
}
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100%!important;
height: 100%;
}
/* -------------------------------------
ELEMENTS
------------------------------------- */
a {
color: #348eda;
}
.btn-primary {
text-decoration: none;
color: #FFF;
background-color: #348eda;
border: solid #348eda;
border-width: 10px 20px;
line-height: 2;
font-weight: bold;
margin-right: 10px;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 25px;
}
.btn-secondary {
text-decoration: none;
color: #FFF;
background-color: #aaa;
border: solid #aaa;
border-width: 10px 20px;
line-height: 2;
font-weight: bold;
margin-right: 10px;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 25px;
}
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.padding {
padding: 10px 0;
}
/* -------------------------------------
BODY
------------------------------------- */
table.body-wrap {
width: 100%;
padding: 20px;
}
table.body-wrap .container {
border-radius: 4px;
}
/* -------------------------------------
FOOTER
------------------------------------- */
table.footer-wrap {
width: 100%;
clear: both!important;
}
.footer-wrap .container {
font-size: 12px;
color: #999;
}
table.footer-wrap a {
color: #666;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1, h2, h3 {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
color: #000;
margin: 40px 0 10px;
line-height: 1.2;
font-weight: 200;
}
h1 {
font-size: 36px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
p, ul, ol {
margin-bottom: 10px;
font-weight: normal;
font-size: 14px;
}
ul li, ol li {
margin-left: 5px;
list-style-position: inside;
}
.word-break {
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
/* This is the dangerous one in WebKit, as it breaks things wherever */
word-break: break-all;
/* Instead use this non-standard one: */
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
/* ---------------------------------------------------
RESPONSIVENESS
Nuke it from orbit. It's the only way to be sure.
------------------------------------------------------ */
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block!important;
max-width: 600px!important;
margin: 0 auto!important; /* makes it centered */
clear: both!important;
}
/* Set the padding on the td rather than the div for Outlook compatibility */
.body-wrap .container {
padding: 20px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
max-width: 600px;
margin: 0 auto;
display: block;
}
/* Let's make sure tables in the content area are 100% wide */
.content table {
width: 100%;
}
</style>
</head>
<body bgcolor="#f6f6f6">
<!-- body -->
<table class="body-wrap" bgcolor="#f6f6f6">
<tr>
<td></td>
<td class="container" bgcolor="#FFFFFF">
<div class="content">
<table>
<tr>
<td>
{% block content %}
{{ content|raw }}
{% endblock content %}
</td>
</tr>
</table>
</div>
</td>
<td></td>
</tr>
</table>
<!-- /body -->
<!-- footer -->
<table class="footer-wrap">
<tr>
<td></td>
<td class="container">
<div class="content">
<table>
<tr>
<td align="center">
{% block footer %}
{{ 'PLUGIN_EMAIL.EMAIL_FOOTER'|t }}
{% endblock footer %}
</td>
</tr>
</table>
</div>
</td>
<td></td>
</tr>
</table>
<!-- /footer -->
</body>
</html>

View File

@@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit73924571ea6ee98bb12d10ff20aff2ab::getLoader();

View File

@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,17 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Grav\\Plugin\\EmailPlugin' => $baseDir . '/email.php',
'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);

View File

@@ -0,0 +1,13 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,29 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'),
'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'),
'Symfony\\Component\\Messenger\\Bridge\\Redis\\' => array($vendorDir . '/symfony/redis-messenger'),
'Symfony\\Component\\Messenger\\Bridge\\Doctrine\\' => array($vendorDir . '/symfony/doctrine-messenger'),
'Symfony\\Component\\Messenger\\Bridge\\Amqp\\' => array($vendorDir . '/symfony/amqp-messenger'),
'Symfony\\Component\\Messenger\\' => array($vendorDir . '/symfony/messenger'),
'Symfony\\Component\\Mailer\\' => array($vendorDir . '/symfony/mailer'),
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Grav\\Plugin\\Email\\' => array($baseDir . '/classes'),
'Grav\\Plugin\\Console\\' => array($baseDir . '/cli'),
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'),
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
);

View File

@@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit73924571ea6ee98bb12d10ff20aff2ab
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit73924571ea6ee98bb12d10ff20aff2ab', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit73924571ea6ee98bb12d10ff20aff2ab', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

View File

@@ -0,0 +1,157 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab
{
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
'Symfony\\Polyfill\\Intl\\Idn\\' => 26,
'Symfony\\Contracts\\Service\\' => 26,
'Symfony\\Contracts\\EventDispatcher\\' => 34,
'Symfony\\Component\\Mime\\' => 23,
'Symfony\\Component\\Messenger\\Bridge\\Redis\\' => 41,
'Symfony\\Component\\Messenger\\Bridge\\Doctrine\\' => 44,
'Symfony\\Component\\Messenger\\Bridge\\Amqp\\' => 40,
'Symfony\\Component\\Messenger\\' => 28,
'Symfony\\Component\\Mailer\\' => 25,
'Symfony\\Component\\EventDispatcher\\' => 34,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\EventDispatcher\\' => 20,
'Psr\\Container\\' => 14,
),
'G' =>
array (
'Grav\\Plugin\\Email\\' => 18,
'Grav\\Plugin\\Console\\' => 20,
),
'E' =>
array (
'Egulias\\EmailValidator\\' => 23,
),
'D' =>
array (
'Doctrine\\Deprecations\\' => 22,
'Doctrine\\Common\\Lexer\\' => 22,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Polyfill\\Intl\\Normalizer\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
),
'Symfony\\Polyfill\\Intl\\Idn\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
),
'Symfony\\Contracts\\Service\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/service-contracts',
),
'Symfony\\Contracts\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts',
),
'Symfony\\Component\\Mime\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/mime',
),
'Symfony\\Component\\Messenger\\Bridge\\Redis\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/redis-messenger',
),
'Symfony\\Component\\Messenger\\Bridge\\Doctrine\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/doctrine-messenger',
),
'Symfony\\Component\\Messenger\\Bridge\\Amqp\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/amqp-messenger',
),
'Symfony\\Component\\Messenger\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/messenger',
),
'Symfony\\Component\\Mailer\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/mailer',
),
'Symfony\\Component\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/event-dispatcher',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
),
'Psr\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
),
'Psr\\Container\\' =>
array (
0 => __DIR__ . '/..' . '/psr/container/src',
),
'Grav\\Plugin\\Email\\' =>
array (
0 => __DIR__ . '/../..' . '/classes',
),
'Grav\\Plugin\\Console\\' =>
array (
0 => __DIR__ . '/../..' . '/cli',
),
'Egulias\\EmailValidator\\' =>
array (
0 => __DIR__ . '/..' . '/egulias/email-validator/src',
),
'Doctrine\\Deprecations\\' =>
array (
0 => __DIR__ . '/..' . '/doctrine/deprecations/src',
),
'Doctrine\\Common\\Lexer\\' =>
array (
0 => __DIR__ . '/..' . '/doctrine/lexer/src',
),
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Grav\\Plugin\\EmailPlugin' => __DIR__ . '/../..' . '/email.php',
'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit73924571ea6ee98bb12d10ff20aff2ab::$classMap;
}, null, ClassLoader::class);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,224 @@
<?php return array(
'root' => array(
'name' => 'getgrav/grav-plugin-email',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => 'ae1a8e9e23373a3fd9d6f6b8c05021cde6ad44e4',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'doctrine/deprecations' => array(
'pretty_version' => '1.1.5',
'version' => '1.1.5.0',
'reference' => '459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38',
'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/deprecations',
'aliases' => array(),
'dev_requirement' => false,
),
'doctrine/lexer' => array(
'pretty_version' => '2.1.1',
'version' => '2.1.1.0',
'reference' => '861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6',
'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/lexer',
'aliases' => array(),
'dev_requirement' => false,
),
'egulias/email-validator' => array(
'pretty_version' => '3.2.6',
'version' => '3.2.6.0',
'reference' => 'e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7',
'type' => 'library',
'install_path' => __DIR__ . '/../egulias/email-validator',
'aliases' => array(),
'dev_requirement' => false,
),
'getgrav/grav-plugin-email' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => 'ae1a8e9e23373a3fd9d6f6b8c05021cde6ad44e4',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/container' => array(
'pretty_version' => '1.1.1',
'version' => '1.1.1.0',
'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/container',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/event-dispatcher' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/event-dispatcher',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/event-dispatcher-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/log' => array(
'pretty_version' => '1.1.4',
'version' => '1.1.4.0',
'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/amqp-messenger' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '822ad5f425ef362580273a175c45aa765220fe73',
'type' => 'symfony-messenger-bridge',
'install_path' => __DIR__ . '/../symfony/amqp-messenger',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v2.5.4',
'version' => '2.5.4.0',
'reference' => '605389f2a7e5625f273b53960dc46aeaf9c62918',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/doctrine-messenger' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '3f5a6e1876fbf57e836ba0a02eb0a636e08c0d96',
'type' => 'symfony-messenger-bridge',
'install_path' => __DIR__ . '/../symfony/doctrine-messenger',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/event-dispatcher' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '72982eb416f61003e9bb6e91f8b3213600dcf9e9',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/event-dispatcher',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/event-dispatcher-contracts' => array(
'pretty_version' => 'v2.5.4',
'version' => '2.5.4.0',
'reference' => 'e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/event-dispatcher-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/event-dispatcher-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '2.0',
),
),
'symfony/mailer' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => 'f732e1fafdf0f4a2d865e91f1018aaca174aeed9',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/mailer',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/messenger' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => 'c21d463ba813a3fe9833f46114310fac99bd66e0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/messenger',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/mime' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '8c1b9b3e5b52981551fc6044539af1d974e39064',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/mime',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-iconv' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-intl-idn' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '9614ac4d8061dc257ecc64cba1b140873dce8ad3',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-intl-normalizer' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '3833d7255cc303546435cb650316bff708a1c75c',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php72' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/redis-messenger' => array(
'pretty_version' => 'v5.4.48',
'version' => '5.4.48.0',
'reference' => 'a097e8c6529a7179a732161bd5368629c6319899',
'type' => 'symfony-messenger-bridge',
'install_path' => __DIR__ . '/../symfony/redis-messenger',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/service-contracts' => array(
'pretty_version' => 'v2.5.4',
'version' => '2.5.4.0',
'reference' => 'f37b419f7aea2e9abf10abd261832cace12e3300',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/service-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View File

@@ -0,0 +1,25 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70306)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.6". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2020-2021 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,218 @@
# Doctrine Deprecations
A small (side-effect free by default) layer on top of
`trigger_error(E_USER_DEPRECATED)` or PSR-3 logging.
- no side-effects by default, making it a perfect fit for libraries that don't know how the error handler works they operate under
- options to avoid having to rely on error handlers global state by using PSR-3 logging
- deduplicate deprecation messages to avoid excessive triggering and reduce overhead
We recommend to collect Deprecations using a PSR logger instead of relying on
the global error handler.
## Usage from consumer perspective:
Enable Doctrine deprecations to be sent to a PSR3 logger:
```php
\Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger);
```
Enable Doctrine deprecations to be sent as `@trigger_error($message, E_USER_DEPRECATED)`
messages by setting the `DOCTRINE_DEPRECATIONS` environment variable to `trigger`.
Alternatively, call:
```php
\Doctrine\Deprecations\Deprecation::enableWithTriggerError();
```
If you only want to enable deprecation tracking, without logging or calling `trigger_error`
then set the `DOCTRINE_DEPRECATIONS` environment variable to `track`.
Alternatively, call:
```php
\Doctrine\Deprecations\Deprecation::enableTrackingDeprecations();
```
Tracking is enabled with all three modes and provides access to all triggered
deprecations and their individual count:
```php
$deprecations = \Doctrine\Deprecations\Deprecation::getTriggeredDeprecations();
foreach ($deprecations as $identifier => $count) {
echo $identifier . " was triggered " . $count . " times\n";
}
```
### Suppressing Specific Deprecations
Disable triggering about specific deprecations:
```php
\Doctrine\Deprecations\Deprecation::ignoreDeprecations("https://link/to/deprecations-description-identifier");
```
Disable all deprecations from a package
```php
\Doctrine\Deprecations\Deprecation::ignorePackage("doctrine/orm");
```
### Other Operations
When used within PHPUnit or other tools that could collect multiple instances of the same deprecations
the deduplication can be disabled:
```php
\Doctrine\Deprecations\Deprecation::withoutDeduplication();
```
Disable deprecation tracking again:
```php
\Doctrine\Deprecations\Deprecation::disable();
```
## Usage from a library/producer perspective:
When you want to unconditionally trigger a deprecation even when called
from the library itself then the `trigger` method is the way to go:
```php
\Doctrine\Deprecations\Deprecation::trigger(
"doctrine/orm",
"https://link/to/deprecations-description",
"message"
);
```
If variable arguments are provided at the end, they are used with `sprintf` on
the message.
```php
\Doctrine\Deprecations\Deprecation::trigger(
"doctrine/orm",
"https://github.com/doctrine/orm/issue/1234",
"message %s %d",
"foo",
1234
);
```
When you want to trigger a deprecation only when it is called by a function
outside of the current package, but not trigger when the package itself is the cause,
then use:
```php
\Doctrine\Deprecations\Deprecation::triggerIfCalledFromOutside(
"doctrine/orm",
"https://link/to/deprecations-description",
"message"
);
```
Based on the issue link each deprecation message is only triggered once per
request.
A limited stacktrace is included in the deprecation message to find the
offending location.
Note: A producer/library should never call `Deprecation::enableWith` methods
and leave the decision how to handle deprecations to application and
frameworks.
## Usage in PHPUnit tests
There is a `VerifyDeprecations` trait that you can use to make assertions on
the occurrence of deprecations within a test.
```php
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
class MyTest extends TestCase
{
use VerifyDeprecations;
public function testSomethingDeprecation()
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234');
triggerTheCodeWithDeprecation();
}
public function testSomethingDeprecationFixed()
{
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234');
triggerTheCodeWithoutDeprecation();
}
}
```
## Displaying deprecations after running a PHPUnit test suite
It is possible to integrate this library with PHPUnit to display all
deprecations triggered during the test suite execution.
```xml
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
displayDetailsOnTestsThatTriggerDeprecations="true"
failOnDeprecation="true"
>
<!-- one attribute to display the deprecations, the other to fail the test suite -->
<php>
<!-- ensures native PHP deprecations are used -->
<server name="DOCTRINE_DEPRECATIONS" value="trigger"/>
</php>
<!-- ensures the @ operator in @trigger_error is ignored -->
<source ignoreSuppressionOfDeprecations="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
```
Note that you can still trigger Deprecations in your code, provided you use the
`#[WithoutErrorHandler]` attribute to disable PHPUnit's error handler for tests
that call it. Be wary that this will disable all error handling, meaning it
will mask any warnings or errors that would otherwise be caught by PHPUnit.
At the moment, it is not possible to disable deduplication with an environment
variable, but you can use a bootstrap file to achieve that:
```php
// tests/bootstrap.php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use Doctrine\Deprecations\Deprecation;
Deprecation::withoutDeduplication();
```
Then, reference that file in your PHPUnit configuration:
```xml
<phpunit
bootstrap="tests/bootstrap.php"
>
</phpunit>
```
## What is a deprecation identifier?
An identifier for deprecations is just a link to any resource, most often a
Github Issue or Pull Request explaining the deprecation and potentially its
alternative.

View File

@@ -0,0 +1,39 @@
{
"name": "doctrine/deprecations",
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"license": "MIT",
"type": "library",
"homepage": "https://www.doctrine-project.org/",
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^13",
"phpstan/phpstan": "1.4.10 || 2.1.11",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
"psr/log": "^1 || ^2 || ^3"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=13"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"DeprecationTests\\": "test_fixtures/src",
"Doctrine\\Foo\\": "test_fixtures/vendor/doctrine/foo"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace Doctrine\Deprecations;
use Psr\Log\LoggerInterface;
use function array_key_exists;
use function array_reduce;
use function assert;
use function debug_backtrace;
use function sprintf;
use function str_replace;
use function strpos;
use function strrpos;
use function substr;
use function trigger_error;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
use const DIRECTORY_SEPARATOR;
use const E_USER_DEPRECATED;
/**
* Manages Deprecation logging in different ways.
*
* By default triggered exceptions are not logged.
*
* To enable different deprecation logging mechanisms you can call the
* following methods:
*
* - Minimal collection of deprecations via getTriggeredDeprecations()
* \Doctrine\Deprecations\Deprecation::enableTrackingDeprecations();
*
* - Uses @trigger_error with E_USER_DEPRECATED
* \Doctrine\Deprecations\Deprecation::enableWithTriggerError();
*
* - Sends deprecation messages via a PSR-3 logger
* \Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger);
*
* Packages that trigger deprecations should use the `trigger()` or
* `triggerIfCalledFromOutside()` methods.
*/
class Deprecation
{
private const TYPE_NONE = 0;
private const TYPE_TRACK_DEPRECATIONS = 1;
private const TYPE_TRIGGER_ERROR = 2;
private const TYPE_PSR_LOGGER = 4;
/** @var int-mask-of<self::TYPE_*>|null */
private static $type;
/** @var LoggerInterface|null */
private static $logger;
/** @var array<string,bool> */
private static $ignoredPackages = [];
/** @var array<string,int> */
private static $triggeredDeprecations = [];
/** @var array<string,bool> */
private static $ignoredLinks = [];
/** @var bool */
private static $deduplication = true;
/**
* Trigger a deprecation for the given package and identfier.
*
* The link should point to a Github issue or Wiki entry detailing the
* deprecation. It is additionally used to de-duplicate the trigger of the
* same deprecation during a request.
*
* @param float|int|string $args
*/
public static function trigger(string $package, string $link, string $message, ...$args): void
{
$type = self::$type ?? self::getTypeFromEnv();
if ($type === self::TYPE_NONE) {
return;
}
if (isset(self::$ignoredLinks[$link])) {
return;
}
if (array_key_exists($link, self::$triggeredDeprecations)) {
self::$triggeredDeprecations[$link]++;
} else {
self::$triggeredDeprecations[$link] = 1;
}
if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) {
return;
}
if (isset(self::$ignoredPackages[$package])) {
return;
}
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$message = sprintf($message, ...$args);
self::delegateTriggerToBackend($message, $backtrace, $link, $package);
}
/**
* Trigger a deprecation for the given package and identifier when called from outside.
*
* "Outside" means we assume that $package is currently installed as a
* dependency and the caller is not a file in that package. When $package
* is installed as a root package then deprecations triggered from the
* tests folder are also considered "outside".
*
* This deprecation method assumes that you are using Composer to install
* the dependency and are using the default /vendor/ folder and not a
* Composer plugin to change the install location. The assumption is also
* that $package is the exact composer packge name.
*
* Compared to {@link trigger()} this method causes some overhead when
* deprecation tracking is enabled even during deduplication, because it
* needs to call {@link debug_backtrace()}
*
* @param float|int|string $args
*/
public static function triggerIfCalledFromOutside(string $package, string $link, string $message, ...$args): void
{
$type = self::$type ?? self::getTypeFromEnv();
if ($type === self::TYPE_NONE) {
return;
}
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
// first check that the caller is not from a tests folder, in which case we always let deprecations pass
if (isset($backtrace[1]['file'], $backtrace[0]['file']) && strpos($backtrace[1]['file'], DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR) === false) {
$path = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $package) . DIRECTORY_SEPARATOR;
if (strpos($backtrace[0]['file'], $path) === false) {
return;
}
if (strpos($backtrace[1]['file'], $path) !== false) {
return;
}
}
if (isset(self::$ignoredLinks[$link])) {
return;
}
if (array_key_exists($link, self::$triggeredDeprecations)) {
self::$triggeredDeprecations[$link]++;
} else {
self::$triggeredDeprecations[$link] = 1;
}
if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) {
return;
}
if (isset(self::$ignoredPackages[$package])) {
return;
}
$message = sprintf($message, ...$args);
self::delegateTriggerToBackend($message, $backtrace, $link, $package);
}
/** @param list<array{function: string, line?: int, file?: string, class?: class-string, type?: string, args?: mixed[], object?: object}> $backtrace */
private static function delegateTriggerToBackend(string $message, array $backtrace, string $link, string $package): void
{
$type = self::$type ?? self::getTypeFromEnv();
if (($type & self::TYPE_PSR_LOGGER) > 0) {
$context = [
'file' => $backtrace[0]['file'] ?? null,
'line' => $backtrace[0]['line'] ?? null,
'package' => $package,
'link' => $link,
];
assert(self::$logger !== null);
self::$logger->notice($message, $context);
}
if (! (($type & self::TYPE_TRIGGER_ERROR) > 0)) {
return;
}
$message .= sprintf(
' (%s:%d called by %s:%d, %s, package %s)',
self::basename($backtrace[0]['file'] ?? 'native code'),
$backtrace[0]['line'] ?? 0,
self::basename($backtrace[1]['file'] ?? 'native code'),
$backtrace[1]['line'] ?? 0,
$link,
$package
);
@trigger_error($message, E_USER_DEPRECATED);
}
/**
* A non-local-aware version of PHPs basename function.
*/
private static function basename(string $filename): string
{
$pos = strrpos($filename, DIRECTORY_SEPARATOR);
if ($pos === false) {
return $filename;
}
return substr($filename, $pos + 1);
}
public static function enableTrackingDeprecations(): void
{
self::$type = self::$type ?? self::getTypeFromEnv();
self::$type |= self::TYPE_TRACK_DEPRECATIONS;
}
public static function enableWithTriggerError(): void
{
self::$type = self::$type ?? self::getTypeFromEnv();
self::$type |= self::TYPE_TRIGGER_ERROR;
}
public static function enableWithPsrLogger(LoggerInterface $logger): void
{
self::$type = self::$type ?? self::getTypeFromEnv();
self::$type |= self::TYPE_PSR_LOGGER;
self::$logger = $logger;
}
public static function withoutDeduplication(): void
{
self::$deduplication = false;
}
public static function disable(): void
{
self::$type = self::TYPE_NONE;
self::$logger = null;
self::$deduplication = true;
self::$ignoredLinks = [];
foreach (self::$triggeredDeprecations as $link => $count) {
self::$triggeredDeprecations[$link] = 0;
}
}
public static function ignorePackage(string $packageName): void
{
self::$ignoredPackages[$packageName] = true;
}
public static function ignoreDeprecations(string ...$links): void
{
foreach ($links as $link) {
self::$ignoredLinks[$link] = true;
}
}
public static function getUniqueTriggeredDeprecationsCount(): int
{
return array_reduce(self::$triggeredDeprecations, static function (int $carry, int $count) {
return $carry + $count;
}, 0);
}
/**
* Returns each triggered deprecation link identifier and the amount of occurrences.
*
* @return array<string,int>
*/
public static function getTriggeredDeprecations(): array
{
return self::$triggeredDeprecations;
}
/** @return int-mask-of<self::TYPE_*> */
private static function getTypeFromEnv(): int
{
switch ($_SERVER['DOCTRINE_DEPRECATIONS'] ?? $_ENV['DOCTRINE_DEPRECATIONS'] ?? null) {
case 'trigger':
self::$type = self::TYPE_TRIGGER_ERROR;
break;
case 'track':
self::$type = self::TYPE_TRACK_DEPRECATIONS;
break;
default:
self::$type = self::TYPE_NONE;
break;
}
return self::$type;
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Doctrine\Deprecations\PHPUnit;
use Doctrine\Deprecations\Deprecation;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
use function sprintf;
trait VerifyDeprecations
{
/** @var array<string,int> */
private $doctrineDeprecationsExpectations = [];
/** @var array<string,int> */
private $doctrineNoDeprecationsExpectations = [];
public function expectDeprecationWithIdentifier(string $identifier): void
{
$this->doctrineDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
}
public function expectNoDeprecationWithIdentifier(string $identifier): void
{
$this->doctrineNoDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
}
/** @before */
#[Before]
public function enableDeprecationTracking(): void
{
Deprecation::enableTrackingDeprecations();
}
/** @after */
#[After]
public function verifyDeprecationsAreTriggered(): void
{
foreach ($this->doctrineDeprecationsExpectations as $identifier => $expectation) {
$actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
$this->assertTrue(
$actualCount > $expectation,
sprintf(
"Expected deprecation with identifier '%s' was not triggered by code executed in test.",
$identifier
)
);
}
foreach ($this->doctrineNoDeprecationsExpectations as $identifier => $expectation) {
$actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
$this->assertTrue(
$actualCount === $expectation,
sprintf(
"Expected deprecation with identifier '%s' was triggered by code executed in test, but expected not to.",
$identifier
)
);
}
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2006-2018 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,9 @@
# Doctrine Lexer
[![Build Status](https://github.com/doctrine/lexer/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/lexer/actions)
Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.
This lexer is used in Doctrine Annotations and in Doctrine ORM (DQL).
https://www.doctrine-project.org/projects/lexer.html

View File

@@ -0,0 +1,14 @@
Note about upgrading: Doctrine uses static and runtime mechanisms to raise
awareness about deprecated code.
- Use of `@deprecated` docblock that is detected by IDEs (like PHPStorm) or
Static Analysis tools (like Psalm, phpstan)
- Use of our low-overhead runtime deprecation API, details:
https://github.com/doctrine/deprecations/
# Upgrade to 2.0.0
`AbstractLexer::glimpse()` and `AbstractLexer::peek()` now return
instances of `Doctrine\Common\Lexer\Token`, which is an array-like class
Using it as an array is deprecated in favor of using properties of that class.
Using `count()` on it is deprecated with no replacement.

View File

@@ -0,0 +1,56 @@
{
"name": "doctrine/lexer",
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
"license": "MIT",
"type": "library",
"keywords": [
"php",
"parser",
"lexer",
"annotations",
"docblock"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
"require": {
"php": "^7.1 || ^8.0",
"doctrine/deprecations": "^1.0"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12",
"phpstan/phpstan": "^1.3",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psalm/plugin-phpunit": "^0.18.3",
"vimeo/psalm": "^4.11 || ^5.21"
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Lexer\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Tests\\Common\\Lexer\\": "tests"
}
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true
},
"sort-packages": true
}
}

View File

@@ -0,0 +1,342 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\Lexer;
use ReflectionClass;
use UnitEnum;
use function get_class;
use function implode;
use function preg_split;
use function sprintf;
use function substr;
use const PREG_SPLIT_DELIM_CAPTURE;
use const PREG_SPLIT_NO_EMPTY;
use const PREG_SPLIT_OFFSET_CAPTURE;
/**
* Base class for writing simple lexers, i.e. for creating small DSLs.
*
* @template T of UnitEnum|string|int
* @template V of string|int
*/
abstract class AbstractLexer
{
/**
* Lexer original input string.
*
* @var string
*/
private $input;
/**
* Array of scanned tokens.
*
* @var list<Token<T, V>>
*/
private $tokens = [];
/**
* Current lexer position in input string.
*
* @var int
*/
private $position = 0;
/**
* Current peek of current lexer position.
*
* @var int
*/
private $peek = 0;
/**
* The next token in the input.
*
* @var Token<T, V>|null
*/
public $lookahead;
/**
* The last matched/seen token.
*
* @var Token<T, V>|null
*/
public $token;
/**
* Composed regex for input parsing.
*
* @var non-empty-string|null
*/
private $regex;
/**
* Sets the input data to be tokenized.
*
* The Lexer is immediately reset and the new input tokenized.
* Any unprocessed tokens from any previous input are lost.
*
* @param string $input The input to be tokenized.
*
* @return void
*/
public function setInput($input)
{
$this->input = $input;
$this->tokens = [];
$this->reset();
$this->scan($input);
}
/**
* Resets the lexer.
*
* @return void
*/
public function reset()
{
$this->lookahead = null;
$this->token = null;
$this->peek = 0;
$this->position = 0;
}
/**
* Resets the peek pointer to 0.
*
* @return void
*/
public function resetPeek()
{
$this->peek = 0;
}
/**
* Resets the lexer position on the input to the given position.
*
* @param int $position Position to place the lexical scanner.
*
* @return void
*/
public function resetPosition($position = 0)
{
$this->position = $position;
}
/**
* Retrieve the original lexer's input until a given position.
*
* @param int $position
*
* @return string
*/
public function getInputUntilPosition($position)
{
return substr($this->input, 0, $position);
}
/**
* Checks whether a given token matches the current lookahead.
*
* @param T $type
*
* @return bool
*
* @psalm-assert-if-true !=null $this->lookahead
*/
public function isNextToken($type)
{
return $this->lookahead !== null && $this->lookahead->isA($type);
}
/**
* Checks whether any of the given tokens matches the current lookahead.
*
* @param list<T> $types
*
* @return bool
*
* @psalm-assert-if-true !=null $this->lookahead
*/
public function isNextTokenAny(array $types)
{
return $this->lookahead !== null && $this->lookahead->isA(...$types);
}
/**
* Moves to the next token in the input string.
*
* @return bool
*
* @psalm-assert-if-true !null $this->lookahead
*/
public function moveNext()
{
$this->peek = 0;
$this->token = $this->lookahead;
$this->lookahead = isset($this->tokens[$this->position])
? $this->tokens[$this->position++] : null;
return $this->lookahead !== null;
}
/**
* Tells the lexer to skip input tokens until it sees a token with the given value.
*
* @param T $type The token type to skip until.
*
* @return void
*/
public function skipUntil($type)
{
while ($this->lookahead !== null && ! $this->lookahead->isA($type)) {
$this->moveNext();
}
}
/**
* Checks if given value is identical to the given token.
*
* @param string $value
* @param int|string $token
*
* @return bool
*/
public function isA($value, $token)
{
return $this->getType($value) === $token;
}
/**
* Moves the lookahead token forward.
*
* @return Token<T, V>|null The next token or NULL if there are no more tokens ahead.
*/
public function peek()
{
if (isset($this->tokens[$this->position + $this->peek])) {
return $this->tokens[$this->position + $this->peek++];
}
return null;
}
/**
* Peeks at the next token, returns it and immediately resets the peek.
*
* @return Token<T, V>|null The next token or NULL if there are no more tokens ahead.
*/
public function glimpse()
{
$peek = $this->peek();
$this->peek = 0;
return $peek;
}
/**
* Scans the input string for tokens.
*
* @param string $input A query string.
*
* @return void
*/
protected function scan($input)
{
if (! isset($this->regex)) {
$this->regex = sprintf(
'/(%s)|%s/%s',
implode(')|(', $this->getCatchablePatterns()),
implode('|', $this->getNonCatchablePatterns()),
$this->getModifiers()
);
}
$flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE;
$matches = preg_split($this->regex, $input, -1, $flags);
if ($matches === false) {
// Work around https://bugs.php.net/78122
$matches = [[$input, 0]];
}
foreach ($matches as $match) {
// Must remain before 'value' assignment since it can change content
$firstMatch = $match[0];
$type = $this->getType($firstMatch);
$this->tokens[] = new Token(
$firstMatch,
$type,
$match[1]
);
}
}
/**
* Gets the literal for a given token.
*
* @param T $token
*
* @return int|string
*/
public function getLiteral($token)
{
if ($token instanceof UnitEnum) {
return get_class($token) . '::' . $token->name;
}
$className = static::class;
$reflClass = new ReflectionClass($className);
$constants = $reflClass->getConstants();
foreach ($constants as $name => $value) {
if ($value === $token) {
return $className . '::' . $name;
}
}
return $token;
}
/**
* Regex modifiers
*
* @return string
*/
protected function getModifiers()
{
return 'iu';
}
/**
* Lexical catchable patterns.
*
* @return string[]
*/
abstract protected function getCatchablePatterns();
/**
* Lexical non-catchable patterns.
*
* @return string[]
*/
abstract protected function getNonCatchablePatterns();
/**
* Retrieve token type. Also processes the token value if necessary.
*
* @param string $value
*
* @return T|null
*
* @param-out V $value
*/
abstract protected function getType(&$value);
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Doctrine\Common\Lexer;
use ArrayAccess;
use Doctrine\Deprecations\Deprecation;
use ReturnTypeWillChange;
use UnitEnum;
use function in_array;
/**
* @template T of UnitEnum|string|int
* @template V of string|int
* @implements ArrayAccess<string,mixed>
*/
final class Token implements ArrayAccess
{
/**
* The string value of the token in the input string
*
* @readonly
* @var V
*/
public $value;
/**
* The type of the token (identifier, numeric, string, input parameter, none)
*
* @readonly
* @var T|null
*/
public $type;
/**
* The position of the token in the input string
*
* @readonly
* @var int
*/
public $position;
/**
* @param V $value
* @param T|null $type
*/
public function __construct($value, $type, int $position)
{
$this->value = $value;
$this->type = $type;
$this->position = $position;
}
/** @param T ...$types */
public function isA(...$types): bool
{
return in_array($this->type, $types, true);
}
/**
* @deprecated Use the value, type or position property instead
* {@inheritDoc}
*/
public function offsetExists($offset): bool
{
Deprecation::trigger(
'doctrine/lexer',
'https://github.com/doctrine/lexer/pull/79',
'Accessing %s properties via ArrayAccess is deprecated, use the value, type or position property instead',
self::class
);
return in_array($offset, ['value', 'type', 'position'], true);
}
/**
* @deprecated Use the value, type or position property instead
* {@inheritDoc}
*
* @param O $offset
*
* @return mixed
* @psalm-return (
* O is 'value'
* ? V
* : (
* O is 'type'
* ? T|null
* : (
* O is 'position'
* ? int
* : mixed
* )
* )
* )
*
* @template O of array-key
*/
#[ReturnTypeWillChange]
public function offsetGet($offset)
{
Deprecation::trigger(
'doctrine/lexer',
'https://github.com/doctrine/lexer/pull/79',
'Accessing %s properties via ArrayAccess is deprecated, use the value, type or position property instead',
self::class
);
return $this->$offset;
}
/**
* @deprecated no replacement planned
* {@inheritDoc}
*/
public function offsetSet($offset, $value): void
{
Deprecation::trigger(
'doctrine/lexer',
'https://github.com/doctrine/lexer/pull/79',
'Setting %s properties via ArrayAccess is deprecated',
self::class
);
$this->$offset = $value;
}
/**
* @deprecated no replacement planned
* {@inheritDoc}
*/
public function offsetUnset($offset): void
{
Deprecation::trigger(
'doctrine/lexer',
'https://github.com/doctrine/lexer/pull/79',
'Setting %s properties via ArrayAccess is deprecated',
self::class
);
$this->$offset = null;
}
}

View File

@@ -0,0 +1,33 @@
# EmailValidator v3 Changelog
## New Features
* Access to local part and domain part from EmailParser
* Validations outside of the scope of the RFC will be considered "extra" validations, thus opening the door for adding new; will live in their own folder "extra" (as requested in #248, #195, #183).
## Breaking changes
* PHP version upgraded to match Symfony's (as of 12/2020).
* DNSCheckValidation now fails for missing MX records. While the RFC argues that the existence of only A records to be valid, starting in v3 they will be considered invalid.
* Emails domain part are now intenteded to be RFC 1035 compliant, rendering previous valid emails (e.g example@examp&) invalid.
## PHP versions upgrade policy
PHP version upgrade requirement will happen via MINOR (3.x) version upgrades of the library, following the adoption level by major frameworks.
## Changes
* #235
* #215
* #130
* #258
* #188
* #181
* #217
* #214
* #249
* #236
* #257
* #210
## Thanks
To contributors, be it with PRs, reporting issues or supporting otherwise.

View File

@@ -0,0 +1,153 @@
# Contributing
When contributing to this repository make sure to follow the Pull request process below.
Reduce to the minimum 3rd party dependencies.
Please note we have a [code of conduct](#Code of Conduct), please follow it in all your interactions with the project.
## Pull Request Process
When doing a PR to v2 remember that you also have to do the PR port to v3, or tests confirming the bug is not reproducible.
1. Supported version is v3. If you are fixing a bug in v2, please port to v3
2. Use the title as a brief description of the changes
3. Describe the changes you are proposing
1. If adding an extra validation state the benefits of adding it and the problem is solving
2. Document in the readme, by adding it to the list
4. Provide appropriate tests for the code you are submitting: aim to keep the existing coverage percentage.
5. Add your Twitter handle (if you have) so we can thank you there.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.
## Code of Conduct
### Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
### Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
### Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at <emailvalidatorrfc.ccreport@gmail.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
#### Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
#### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
#### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
#### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
#### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -0,0 +1,19 @@
Copyright (c) 2013-2022 Eduardo Gulias Davis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,37 @@
{
"name": "egulias/email-validator",
"description": "A library for validating emails against several RFCs",
"homepage": "https://github.com/egulias/EmailValidator",
"keywords": ["email", "validation", "validator", "emailvalidation", "emailvalidator"],
"license": "MIT",
"authors": [
{"name": "Eduardo Gulias Davis"}
],
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"require": {
"php": ">=7.2",
"doctrine/lexer": "^1.2|^2",
"symfony/polyfill-intl-idn": "^1.15"
},
"require-dev": {
"phpunit/phpunit": "^8.5.8|^9.3.3",
"vimeo/psalm": "^4"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
},
"autoload": {
"psr-4": {
"Egulias\\EmailValidator\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Egulias\\EmailValidator\\Tests\\": "tests"
}
}
}

View File

@@ -0,0 +1,345 @@
<?php
namespace Egulias\EmailValidator;
use Doctrine\Common\Lexer\AbstractLexer;
use Doctrine\Common\Lexer\Token;
/**
* @extends AbstractLexer<int, string>
*/
class EmailLexer extends AbstractLexer
{
//ASCII values
public const S_EMPTY = null;
public const C_NUL = 0;
public const S_HTAB = 9;
public const S_LF = 10;
public const S_CR = 13;
public const S_SP = 32;
public const EXCLAMATION = 33;
public const S_DQUOTE = 34;
public const NUMBER_SIGN = 35;
public const DOLLAR = 36;
public const PERCENTAGE = 37;
public const AMPERSAND = 38;
public const S_SQUOTE = 39;
public const S_OPENPARENTHESIS = 40;
public const S_CLOSEPARENTHESIS = 41;
public const ASTERISK = 42;
public const S_PLUS = 43;
public const S_COMMA = 44;
public const S_HYPHEN = 45;
public const S_DOT = 46;
public const S_SLASH = 47;
public const S_COLON = 58;
public const S_SEMICOLON = 59;
public const S_LOWERTHAN = 60;
public const S_EQUAL = 61;
public const S_GREATERTHAN = 62;
public const QUESTIONMARK = 63;
public const S_AT = 64;
public const S_OPENBRACKET = 91;
public const S_BACKSLASH = 92;
public const S_CLOSEBRACKET = 93;
public const CARET = 94;
public const S_UNDERSCORE = 95;
public const S_BACKTICK = 96;
public const S_OPENCURLYBRACES = 123;
public const S_PIPE = 124;
public const S_CLOSECURLYBRACES = 125;
public const S_TILDE = 126;
public const C_DEL = 127;
public const INVERT_QUESTIONMARK= 168;
public const INVERT_EXCLAMATION = 173;
public const GENERIC = 300;
public const S_IPV6TAG = 301;
public const INVALID = 302;
public const CRLF = 1310;
public const S_DOUBLECOLON = 5858;
public const ASCII_INVALID_FROM = 127;
public const ASCII_INVALID_TO = 199;
/**
* US-ASCII visible characters not valid for atext (@link http://tools.ietf.org/html/rfc5322#section-3.2.3)
*
* @var array
*/
protected $charValue = [
'{' => self::S_OPENCURLYBRACES,
'}' => self::S_CLOSECURLYBRACES,
'(' => self::S_OPENPARENTHESIS,
')' => self::S_CLOSEPARENTHESIS,
'<' => self::S_LOWERTHAN,
'>' => self::S_GREATERTHAN,
'[' => self::S_OPENBRACKET,
']' => self::S_CLOSEBRACKET,
':' => self::S_COLON,
';' => self::S_SEMICOLON,
'@' => self::S_AT,
'\\' => self::S_BACKSLASH,
'/' => self::S_SLASH,
',' => self::S_COMMA,
'.' => self::S_DOT,
"'" => self::S_SQUOTE,
"`" => self::S_BACKTICK,
'"' => self::S_DQUOTE,
'-' => self::S_HYPHEN,
'::' => self::S_DOUBLECOLON,
' ' => self::S_SP,
"\t" => self::S_HTAB,
"\r" => self::S_CR,
"\n" => self::S_LF,
"\r\n" => self::CRLF,
'IPv6' => self::S_IPV6TAG,
'' => self::S_EMPTY,
'\0' => self::C_NUL,
'*' => self::ASTERISK,
'!' => self::EXCLAMATION,
'&' => self::AMPERSAND,
'^' => self::CARET,
'$' => self::DOLLAR,
'%' => self::PERCENTAGE,
'~' => self::S_TILDE,
'|' => self::S_PIPE,
'_' => self::S_UNDERSCORE,
'=' => self::S_EQUAL,
'+' => self::S_PLUS,
'¿' => self::INVERT_QUESTIONMARK,
'?' => self::QUESTIONMARK,
'#' => self::NUMBER_SIGN,
'¡' => self::INVERT_EXCLAMATION,
];
public const INVALID_CHARS_REGEX = "/[^\p{S}\p{C}\p{Cc}]+/iu";
public const VALID_UTF8_REGEX = '/\p{Cc}+/u';
public const CATCHABLE_PATTERNS = [
'[a-zA-Z]+[46]?', //ASCII and domain literal
'[^\x00-\x7F]', //UTF-8
'[0-9]+',
'\r\n',
'::',
'\s+?',
'.',
];
public const NON_CATCHABLE_PATTERNS = [
'[\xA0-\xff]+',
];
public const MODIFIERS = 'iu';
/** @var bool */
protected $hasInvalidTokens = false;
/**
* @var array
*
* @psalm-var array{value:string, type:null|int, position:int}|array<empty, empty>
*/
protected $previous = [];
/**
* The last matched/seen token.
*
* @var array|Token
*
* @psalm-suppress NonInvariantDocblockPropertyType
* @psalm-var array{value:string, type:null|int, position:int}|Token<int, string>
*/
public $token;
/**
* The next token in the input.
*
* @var array|Token|null
*
* @psalm-suppress NonInvariantDocblockPropertyType
* @psalm-var array{position: int, type: int|null|string, value: int|string}|Token<int, string>|null
*/
public $lookahead;
/** @psalm-var array{value:'', type:null, position:0} */
private static $nullToken = [
'value' => '',
'type' => null,
'position' => 0,
];
/** @var string */
private $accumulator = '';
/** @var bool */
private $hasToRecord = false;
public function __construct()
{
$this->previous = $this->token = self::$nullToken;
$this->lookahead = null;
}
public function reset() : void
{
$this->hasInvalidTokens = false;
parent::reset();
$this->previous = $this->token = self::$nullToken;
}
/**
* @param int $type
* @throws \UnexpectedValueException
* @return boolean
*
* @psalm-suppress InvalidScalarArgument
*/
public function find($type) : bool
{
$search = clone $this;
$search->skipUntil($type);
if (!$search->lookahead) {
throw new \UnexpectedValueException($type . ' not found');
}
return true;
}
/**
* moveNext
*
* @return boolean
*/
public function moveNext() : bool
{
if ($this->hasToRecord && $this->previous === self::$nullToken) {
$this->accumulator .= ((array) $this->token)['value'];
}
$this->previous = (array) $this->token;
if($this->lookahead === null) {
$this->lookahead = self::$nullToken;
}
$hasNext = parent::moveNext();
if ($this->hasToRecord) {
$this->accumulator .= ((array) $this->token)['value'];
}
return $hasNext;
}
/**
* Retrieve token type. Also processes the token value if necessary.
*
* @param string $value
* @throws \InvalidArgumentException
* @return integer
*/
protected function getType(&$value)
{
$encoded = $value;
if (mb_detect_encoding($value, 'auto', true) !== 'UTF-8') {
$encoded = mb_convert_encoding($value, 'UTF-8', 'Windows-1252');
}
if ($this->isValid($encoded)) {
return $this->charValue[$encoded];
}
if ($this->isNullType($encoded)) {
return self::C_NUL;
}
if ($this->isInvalidChar($encoded)) {
$this->hasInvalidTokens = true;
return self::INVALID;
}
return self::GENERIC;
}
protected function isValid(string $value) : bool
{
return isset($this->charValue[$value]);
}
protected function isNullType(string $value) : bool
{
return $value === "\0";
}
protected function isInvalidChar(string $value) : bool
{
return !preg_match(self::INVALID_CHARS_REGEX, $value);
}
protected function isUTF8Invalid(string $value) : bool
{
return preg_match(self::VALID_UTF8_REGEX, $value) !== false;
}
public function hasInvalidTokens() : bool
{
return $this->hasInvalidTokens;
}
/**
* getPrevious
*
* @return array
*/
public function getPrevious() : array
{
return $this->previous;
}
/**
* Lexical catchable patterns.
*
* @return string[]
*/
protected function getCatchablePatterns() : array
{
return self::CATCHABLE_PATTERNS;
}
/**
* Lexical non-catchable patterns.
*
* @return string[]
*/
protected function getNonCatchablePatterns() : array
{
return self::NON_CATCHABLE_PATTERNS;
}
protected function getModifiers() : string
{
return self::MODIFIERS;
}
public function getAccumulatedValues() : string
{
return $this->accumulator;
}
public function startRecording() : void
{
$this->hasToRecord = true;
}
public function stopRecording() : void
{
$this->hasToRecord = false;
}
public function clearRecorded() : void
{
$this->accumulator = '';
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Egulias\EmailValidator;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Parser\LocalPart;
use Egulias\EmailValidator\Parser\DomainPart;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\EmailTooLong;
use Egulias\EmailValidator\Result\Reason\NoLocalPart;
class EmailParser extends Parser
{
public const EMAIL_MAX_LENGTH = 254;
/**
* @var string
*/
protected $domainPart = '';
/**
* @var string
*/
protected $localPart = '';
public function parse(string $str) : Result
{
$result = parent::parse($str);
$this->addLongEmailWarning($this->localPart, $this->domainPart);
return $result;
}
protected function preLeftParsing(): Result
{
if (!$this->hasAtToken()) {
return new InvalidEmail(new NoLocalPart(), $this->lexer->token["value"]);
}
return new ValidEmail();
}
protected function parseLeftFromAt(): Result
{
return $this->processLocalPart();
}
protected function parseRightFromAt(): Result
{
return $this->processDomainPart();
}
private function processLocalPart() : Result
{
$localPartParser = new LocalPart($this->lexer);
$localPartResult = $localPartParser->parse();
$this->localPart = $localPartParser->localPart();
$this->warnings = array_merge($localPartParser->getWarnings(), $this->warnings);
return $localPartResult;
}
private function processDomainPart() : Result
{
$domainPartParser = new DomainPart($this->lexer);
$domainPartResult = $domainPartParser->parse();
$this->domainPart = $domainPartParser->domainPart();
$this->warnings = array_merge($domainPartParser->getWarnings(), $this->warnings);
return $domainPartResult;
}
public function getDomainPart() : string
{
return $this->domainPart;
}
public function getLocalPart() : string
{
return $this->localPart;
}
private function addLongEmailWarning(string $localPart, string $parsedDomainPart) : void
{
if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAIL_MAX_LENGTH) {
$this->warnings[EmailTooLong::CODE] = new EmailTooLong();
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Egulias\EmailValidator;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Validation\EmailValidation;
class EmailValidator
{
/**
* @var EmailLexer
*/
private $lexer;
/**
* @var Warning\Warning[]
*/
private $warnings = [];
/**
* @var ?InvalidEmail
*/
private $error;
public function __construct()
{
$this->lexer = new EmailLexer();
}
/**
* @param string $email
* @param EmailValidation $emailValidation
* @return bool
*/
public function isValid(string $email, EmailValidation $emailValidation)
{
$isValid = $emailValidation->isValid($email, $this->lexer);
$this->warnings = $emailValidation->getWarnings();
$this->error = $emailValidation->getError();
return $isValid;
}
/**
* @return boolean
*/
public function hasWarnings()
{
return !empty($this->warnings);
}
/**
* @return array
*/
public function getWarnings()
{
return $this->warnings;
}
/**
* @return InvalidEmail|null
*/
public function getError()
{
return $this->error;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Egulias\EmailValidator;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Parser\IDLeftPart;
use Egulias\EmailValidator\Parser\IDRightPart;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\EmailTooLong;
use Egulias\EmailValidator\Result\Reason\NoLocalPart;
class MessageIDParser extends Parser
{
public const EMAILID_MAX_LENGTH = 254;
/**
* @var string
*/
protected $idLeft = '';
/**
* @var string
*/
protected $idRight = '';
public function parse(string $str) : Result
{
$result = parent::parse($str);
$this->addLongEmailWarning($this->idLeft, $this->idRight);
return $result;
}
protected function preLeftParsing(): Result
{
if (!$this->hasAtToken()) {
return new InvalidEmail(new NoLocalPart(), $this->lexer->token["value"]);
}
return new ValidEmail();
}
protected function parseLeftFromAt(): Result
{
return $this->processIDLeft();
}
protected function parseRightFromAt(): Result
{
return $this->processIDRight();
}
private function processIDLeft() : Result
{
$localPartParser = new IDLeftPart($this->lexer);
$localPartResult = $localPartParser->parse();
$this->idLeft = $localPartParser->localPart();
$this->warnings = array_merge($localPartParser->getWarnings(), $this->warnings);
return $localPartResult;
}
private function processIDRight() : Result
{
$domainPartParser = new IDRightPart($this->lexer);
$domainPartResult = $domainPartParser->parse();
$this->idRight = $domainPartParser->domainPart();
$this->warnings = array_merge($domainPartParser->getWarnings(), $this->warnings);
return $domainPartResult;
}
public function getLeftPart() : string
{
return $this->idLeft;
}
public function getRightPart() : string
{
return $this->idRight;
}
private function addLongEmailWarning(string $localPart, string $parsedDomainPart) : void
{
if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAILID_MAX_LENGTH) {
$this->warnings[EmailTooLong::CODE] = new EmailTooLong();
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Egulias\EmailValidator;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
abstract class Parser
{
/**
* @var Warning\Warning[]
*/
protected $warnings = [];
/**
* @var EmailLexer
*/
protected $lexer;
/**
* id-left "@" id-right
*/
abstract protected function parseRightFromAt() : Result;
abstract protected function parseLeftFromAt() : Result;
abstract protected function preLeftParsing() : Result;
public function __construct(EmailLexer $lexer)
{
$this->lexer = $lexer;
}
public function parse(string $str) : Result
{
$this->lexer->setInput($str);
if ($this->lexer->hasInvalidTokens()) {
return new InvalidEmail(new ExpectingATEXT("Invalid tokens found"), $this->lexer->token["value"]);
}
$preParsingResult = $this->preLeftParsing();
if ($preParsingResult->isInvalid()) {
return $preParsingResult;
}
$localPartResult = $this->parseLeftFromAt();
if ($localPartResult->isInvalid()) {
return $localPartResult;
}
$domainPartResult = $this->parseRightFromAt();
if ($domainPartResult->isInvalid()) {
return $domainPartResult;
}
return new ValidEmail();
}
/**
* @return Warning\Warning[]
*/
public function getWarnings() : array
{
return $this->warnings;
}
protected function hasAtToken() : bool
{
$this->lexer->moveNext();
$this->lexer->moveNext();
return ((array) $this->lexer->token)['type'] !== EmailLexer::S_AT;
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Warning\QuotedPart;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Parser\CommentStrategy\CommentStrategy;
use Egulias\EmailValidator\Result\Reason\UnclosedComment;
use Egulias\EmailValidator\Result\Reason\UnOpenedComment;
use Egulias\EmailValidator\Warning\Comment as WarningComment;
class Comment extends PartParser
{
/**
* @var int
*/
private $openedParenthesis = 0;
/**
* @var CommentStrategy
*/
private $commentStrategy;
public function __construct(EmailLexer $lexer, CommentStrategy $commentStrategy)
{
$this->lexer = $lexer;
$this->commentStrategy = $commentStrategy;
}
public function parse() : Result
{
if (((array) $this->lexer->token)['type'] === EmailLexer::S_OPENPARENTHESIS) {
$this->openedParenthesis++;
if($this->noClosingParenthesis()) {
return new InvalidEmail(new UnclosedComment(), ((array) $this->lexer->token)['value']);
}
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
return new InvalidEmail(new UnOpenedComment(), ((array) $this->lexer->token)['value']);
}
$this->warnings[WarningComment::CODE] = new WarningComment();
$moreTokens = true;
while ($this->commentStrategy->exitCondition($this->lexer, $this->openedParenthesis) && $moreTokens){
if ($this->lexer->isNextToken(EmailLexer::S_OPENPARENTHESIS)) {
$this->openedParenthesis++;
}
$this->warnEscaping();
if($this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
$this->openedParenthesis--;
}
$moreTokens = $this->lexer->moveNext();
}
if($this->openedParenthesis >= 1) {
return new InvalidEmail(new UnclosedComment(), ((array) $this->lexer->token)['value']);
}
if ($this->openedParenthesis < 0) {
return new InvalidEmail(new UnOpenedComment(), ((array) $this->lexer->token)['value']);
}
$finalValidations = $this->commentStrategy->endOfLoopValidations($this->lexer);
$this->warnings = array_merge($this->warnings, $this->commentStrategy->getWarnings());
return $finalValidations;
}
/**
* @return bool
*/
private function warnEscaping() : bool
{
//Backslash found
if (((array) $this->lexer->token)['type'] !== EmailLexer::S_BACKSLASH) {
return false;
}
if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) {
return false;
}
$this->warnings[QuotedPart::CODE] =
new QuotedPart($this->lexer->getPrevious()['type'], ((array) $this->lexer->token)['type']);
return true;
}
private function noClosingParenthesis() : bool
{
try {
$this->lexer->find(EmailLexer::S_CLOSEPARENTHESIS);
return false;
} catch (\RuntimeException $e) {
return true;
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Egulias\EmailValidator\Parser\CommentStrategy;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
interface CommentStrategy
{
/**
* Return "true" to continue, "false" to exit
*/
public function exitCondition(EmailLexer $lexer, int $openedParenthesis) : bool;
public function endOfLoopValidations(EmailLexer $lexer) : Result;
public function getWarnings() : array;
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Egulias\EmailValidator\Parser\CommentStrategy;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
class DomainComment implements CommentStrategy
{
public function exitCondition(EmailLexer $lexer, int $openedParenthesis) : bool
{
if (($openedParenthesis === 0 && $lexer->isNextToken(EmailLexer::S_DOT))){ // || !$internalLexer->moveNext()) {
return false;
}
return true;
}
public function endOfLoopValidations(EmailLexer $lexer) : Result
{
//test for end of string
if (!$lexer->isNextToken(EmailLexer::S_DOT)) {
return new InvalidEmail(new ExpectingATEXT('DOT not found near CLOSEPARENTHESIS'), ((array) $lexer->token)['value']);
}
//add warning
//Address is valid within the message but cannot be used unmodified for the envelope
return new ValidEmail();
}
public function getWarnings(): array
{
return [];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Egulias\EmailValidator\Parser\CommentStrategy;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Warning\CFWSNearAt;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
class LocalComment implements CommentStrategy
{
/**
* @var array
*/
private $warnings = [];
public function exitCondition(EmailLexer $lexer, int $openedParenthesis) : bool
{
return !$lexer->isNextToken(EmailLexer::S_AT);
}
public function endOfLoopValidations(EmailLexer $lexer) : Result
{
if (!$lexer->isNextToken(EmailLexer::S_AT)) {
return new InvalidEmail(new ExpectingATEXT('ATEX is not expected after closing comments'), ((array) $lexer->token)['value']);
}
$this->warnings[CFWSNearAt::CODE] = new CFWSNearAt();
return new ValidEmail();
}
public function getWarnings(): array
{
return $this->warnings;
}
}

View File

@@ -0,0 +1,211 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\CFWSWithFWS;
use Egulias\EmailValidator\Warning\IPV6BadChar;
use Egulias\EmailValidator\Result\Reason\CRNoLF;
use Egulias\EmailValidator\Warning\IPV6ColonEnd;
use Egulias\EmailValidator\Warning\IPV6MaxGroups;
use Egulias\EmailValidator\Warning\ObsoleteDTEXT;
use Egulias\EmailValidator\Warning\AddressLiteral;
use Egulias\EmailValidator\Warning\IPV6ColonStart;
use Egulias\EmailValidator\Warning\IPV6Deprecated;
use Egulias\EmailValidator\Warning\IPV6GroupCount;
use Egulias\EmailValidator\Warning\IPV6DoubleColon;
use Egulias\EmailValidator\Result\Reason\ExpectingDTEXT;
use Egulias\EmailValidator\Result\Reason\UnusualElements;
use Egulias\EmailValidator\Warning\DomainLiteral as WarningDomainLiteral;
class DomainLiteral extends PartParser
{
public const IPV4_REGEX = '/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
public const OBSOLETE_WARNINGS = [
EmailLexer::INVALID,
EmailLexer::C_DEL,
EmailLexer::S_LF,
EmailLexer::S_BACKSLASH
];
public function parse() : Result
{
$this->addTagWarnings();
$IPv6TAG = false;
$addressLiteral = '';
do {
if (((array) $this->lexer->token)['type'] === EmailLexer::C_NUL) {
return new InvalidEmail(new ExpectingDTEXT(), ((array) $this->lexer->token)['value']);
}
$this->addObsoleteWarnings();
if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENBRACKET, EmailLexer::S_OPENBRACKET))) {
return new InvalidEmail(new ExpectingDTEXT(), ((array) $this->lexer->token)['value']);
}
if ($this->lexer->isNextTokenAny(
array(EmailLexer::S_HTAB, EmailLexer::S_SP, EmailLexer::CRLF)
)) {
$this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
$this->parseFWS();
}
if ($this->lexer->isNextToken(EmailLexer::S_CR)) {
return new InvalidEmail(new CRNoLF(), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_BACKSLASH) {
return new InvalidEmail(new UnusualElements(((array) $this->lexer->token)['value']), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_IPV6TAG) {
$IPv6TAG = true;
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_CLOSEBRACKET) {
break;
}
$addressLiteral .= ((array) $this->lexer->token)['value'];
} while ($this->lexer->moveNext());
//Encapsulate
$addressLiteral = str_replace('[', '', $addressLiteral);
$isAddressLiteralIPv4 = $this->checkIPV4Tag($addressLiteral);
if (!$isAddressLiteralIPv4) {
return new ValidEmail();
} else {
$addressLiteral = $this->convertIPv4ToIPv6($addressLiteral);
}
if (!$IPv6TAG) {
$this->warnings[WarningDomainLiteral::CODE] = new WarningDomainLiteral();
return new ValidEmail();
}
$this->warnings[AddressLiteral::CODE] = new AddressLiteral();
$this->checkIPV6Tag($addressLiteral);
return new ValidEmail();
}
/**
* @param string $addressLiteral
* @param int $maxGroups
*/
public function checkIPV6Tag($addressLiteral, $maxGroups = 8) : void
{
$prev = $this->lexer->getPrevious();
if ($prev['type'] === EmailLexer::S_COLON) {
$this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd();
}
$IPv6 = substr($addressLiteral, 5);
//Daniel Marschall's new IPv6 testing strategy
$matchesIP = explode(':', $IPv6);
$groupCount = count($matchesIP);
$colons = strpos($IPv6, '::');
if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) {
$this->warnings[IPV6BadChar::CODE] = new IPV6BadChar();
}
if ($colons === false) {
// We need exactly the right number of groups
if ($groupCount !== $maxGroups) {
$this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount();
}
return;
}
if ($colons !== strrpos($IPv6, '::')) {
$this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon();
return;
}
if ($colons === 0 || $colons === (strlen($IPv6) - 2)) {
// RFC 4291 allows :: at the start or end of an address
//with 7 other groups in addition
++$maxGroups;
}
if ($groupCount > $maxGroups) {
$this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups();
} elseif ($groupCount === $maxGroups) {
$this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated();
}
}
public function convertIPv4ToIPv6(string $addressLiteralIPv4) : string
{
$matchesIP = [];
$IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteralIPv4, $matchesIP);
// Extract IPv4 part from the end of the address-literal (if there is one)
if ($IPv4Match > 0) {
$index = (int) strrpos($addressLiteralIPv4, $matchesIP[0]);
//There's a match but it is at the start
if ($index > 0) {
// Convert IPv4 part to IPv6 format for further testing
return substr($addressLiteralIPv4, 0, $index) . '0:0';
}
}
return $addressLiteralIPv4;
}
/**
* @param string $addressLiteral
*
* @return bool
*/
protected function checkIPV4Tag($addressLiteral) : bool
{
$matchesIP = [];
$IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteral, $matchesIP);
// Extract IPv4 part from the end of the address-literal (if there is one)
if ($IPv4Match > 0) {
$index = strrpos($addressLiteral, $matchesIP[0]);
//There's a match but it is at the start
if ($index === 0) {
$this->warnings[AddressLiteral::CODE] = new AddressLiteral();
return false;
}
}
return true;
}
private function addObsoleteWarnings() : void
{
if(in_array(((array) $this->lexer->token)['type'], self::OBSOLETE_WARNINGS)) {
$this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
}
}
private function addTagWarnings() : void
{
if ($this->lexer->isNextToken(EmailLexer::S_COLON)) {
$this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
}
if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) {
$lexer = clone $this->lexer;
$lexer->moveNext();
if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) {
$this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
}
}
}
}

View File

@@ -0,0 +1,316 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Doctrine\Common\Lexer\Token;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Warning\TLD;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\DotAtEnd;
use Egulias\EmailValidator\Result\Reason\DotAtStart;
use Egulias\EmailValidator\Warning\DeprecatedComment;
use Egulias\EmailValidator\Result\Reason\CRLFAtTheEnd;
use Egulias\EmailValidator\Result\Reason\LabelTooLong;
use Egulias\EmailValidator\Result\Reason\NoDomainPart;
use Egulias\EmailValidator\Result\Reason\ConsecutiveAt;
use Egulias\EmailValidator\Result\Reason\DomainTooLong;
use Egulias\EmailValidator\Result\Reason\CharNotAllowed;
use Egulias\EmailValidator\Result\Reason\DomainHyphened;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Parser\CommentStrategy\DomainComment;
use Egulias\EmailValidator\Result\Reason\ExpectingDomainLiteralClose;
use Egulias\EmailValidator\Parser\DomainLiteral as DomainLiteralParser;
class DomainPart extends PartParser
{
public const DOMAIN_MAX_LENGTH = 253;
public const LABEL_MAX_LENGTH = 63;
/**
* @var string
*/
protected $domainPart = '';
/**
* @var string
*/
protected $label = '';
public function parse() : Result
{
$this->lexer->clearRecorded();
$this->lexer->startRecording();
$this->lexer->moveNext();
$domainChecks = $this->performDomainStartChecks();
if ($domainChecks->isInvalid()) {
return $domainChecks;
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_AT) {
return new InvalidEmail(new ConsecutiveAt(), ((array) $this->lexer->token)['value']);
}
$result = $this->doParseDomainPart();
if ($result->isInvalid()) {
return $result;
}
$end = $this->checkEndOfDomain();
if ($end->isInvalid()) {
return $end;
}
$this->lexer->stopRecording();
$this->domainPart = $this->lexer->getAccumulatedValues();
$length = strlen($this->domainPart);
if ($length > self::DOMAIN_MAX_LENGTH) {
return new InvalidEmail(new DomainTooLong(), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
private function checkEndOfDomain() : Result
{
$prev = $this->lexer->getPrevious();
if ($prev['type'] === EmailLexer::S_DOT) {
return new InvalidEmail(new DotAtEnd(), ((array) $this->lexer->token)['value']);
}
if ($prev['type'] === EmailLexer::S_HYPHEN) {
return new InvalidEmail(new DomainHyphened('Hypen found at the end of the domain'), $prev['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_SP) {
return new InvalidEmail(new CRLFAtTheEnd(), $prev['value']);
}
return new ValidEmail();
}
private function performDomainStartChecks() : Result
{
$invalidTokens = $this->checkInvalidTokensAfterAT();
if ($invalidTokens->isInvalid()) {
return $invalidTokens;
}
$missingDomain = $this->checkEmptyDomain();
if ($missingDomain->isInvalid()) {
return $missingDomain;
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_OPENPARENTHESIS) {
$this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
}
return new ValidEmail();
}
private function checkEmptyDomain() : Result
{
$thereIsNoDomain = ((array) $this->lexer->token)['type'] === EmailLexer::S_EMPTY ||
(((array) $this->lexer->token)['type'] === EmailLexer::S_SP &&
!$this->lexer->isNextToken(EmailLexer::GENERIC));
if ($thereIsNoDomain) {
return new InvalidEmail(new NoDomainPart(), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
private function checkInvalidTokensAfterAT() : Result
{
if (((array) $this->lexer->token)['type'] === EmailLexer::S_DOT) {
return new InvalidEmail(new DotAtStart(), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_HYPHEN) {
return new InvalidEmail(new DomainHyphened('After AT'), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
protected function parseComments(): Result
{
$commentParser = new Comment($this->lexer, new DomainComment());
$result = $commentParser->parse();
$this->warnings = array_merge($this->warnings, $commentParser->getWarnings());
return $result;
}
protected function doParseDomainPart() : Result
{
$tldMissing = true;
$hasComments = false;
$domain = '';
do {
$prev = $this->lexer->getPrevious();
$notAllowedChars = $this->checkNotAllowedChars($this->lexer->token);
if ($notAllowedChars->isInvalid()) {
return $notAllowedChars;
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_OPENPARENTHESIS ||
((array) $this->lexer->token)['type'] === EmailLexer::S_CLOSEPARENTHESIS ) {
$hasComments = true;
$commentsResult = $this->parseComments();
//Invalid comment parsing
if($commentsResult->isInvalid()) {
return $commentsResult;
}
}
$dotsResult = $this->checkConsecutiveDots();
if ($dotsResult->isInvalid()) {
return $dotsResult;
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_OPENBRACKET) {
$literalResult = $this->parseDomainLiteral();
$this->addTLDWarnings($tldMissing);
return $literalResult;
}
$labelCheck = $this->checkLabelLength();
if ($labelCheck->isInvalid()) {
return $labelCheck;
}
$FwsResult = $this->parseFWS();
if($FwsResult->isInvalid()) {
return $FwsResult;
}
$domain .= ((array) $this->lexer->token)['value'];
if (((array) $this->lexer->token)['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
$tldMissing = false;
}
$exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments);
if ($exceptionsResult->isInvalid()) {
return $exceptionsResult;
}
$this->lexer->moveNext();
} while (null !== ((array) $this->lexer->token)['type']);
$labelCheck = $this->checkLabelLength(true);
if ($labelCheck->isInvalid()) {
return $labelCheck;
}
$this->addTLDWarnings($tldMissing);
$this->domainPart = $domain;
return new ValidEmail();
}
/**
* @psalm-param array|Token<int, string> $token
*/
private function checkNotAllowedChars($token) : Result
{
$notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
if (isset($notAllowed[((array) $token)['type']])) {
return new InvalidEmail(new CharNotAllowed(), ((array) $token)['value']);
}
return new ValidEmail();
}
/**
* @return Result
*/
protected function parseDomainLiteral() : Result
{
try {
$this->lexer->find(EmailLexer::S_CLOSEBRACKET);
} catch (\RuntimeException $e) {
return new InvalidEmail(new ExpectingDomainLiteralClose(), ((array) $this->lexer->token)['value']);
}
$domainLiteralParser = new DomainLiteralParser($this->lexer);
$result = $domainLiteralParser->parse();
$this->warnings = array_merge($this->warnings, $domainLiteralParser->getWarnings());
return $result;
}
protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result
{
if (((array) $this->lexer->token)['type'] === EmailLexer::S_OPENBRACKET && $prev['type'] !== EmailLexer::S_AT) {
return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_BACKSLASH
&& $this->lexer->isNextToken(EmailLexer::GENERIC)) {
return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), ((array) $this->lexer->token)['value']);
}
return $this->validateTokens($hasComments);
}
protected function validateTokens(bool $hasComments) : Result
{
$validDomainTokens = array(
EmailLexer::GENERIC => true,
EmailLexer::S_HYPHEN => true,
EmailLexer::S_DOT => true,
);
if ($hasComments) {
$validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
$validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
}
if (!isset($validDomainTokens[((array) $this->lexer->token)['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . ((array) $this->lexer->token)['value']), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
private function checkLabelLength(bool $isEndOfDomain = false) : Result
{
if (((array) $this->lexer->token)['type'] === EmailLexer::S_DOT || $isEndOfDomain) {
if ($this->isLabelTooLong($this->label)) {
return new InvalidEmail(new LabelTooLong(), ((array) $this->lexer->token)['value']);
}
$this->label = '';
}
$this->label .= ((array) $this->lexer->token)['value'];
return new ValidEmail();
}
private function isLabelTooLong(string $label) : bool
{
if (preg_match('/[^\x00-\x7F]/', $label)) {
idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG);
}
return strlen($label) > self::LABEL_MAX_LENGTH;
}
private function addTLDWarnings(bool $isTLDMissing) : void
{
if ($isTLDMissing) {
$this->warnings[TLD::CODE] = new TLD();
}
}
public function domainPart() : string
{
return $this->domainPart;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\CFWSWithFWS;
use Egulias\EmailValidator\Warning\QuotedString;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Result\Reason\UnclosedQuotedString;
use Egulias\EmailValidator\Result\Result;
class DoubleQuote extends PartParser
{
public function parse() : Result
{
$validQuotedString = $this->checkDQUOTE();
if($validQuotedString->isInvalid()) return $validQuotedString;
$special = [
EmailLexer::S_CR => true,
EmailLexer::S_HTAB => true,
EmailLexer::S_LF => true
];
$invalid = [
EmailLexer::C_NUL => true,
EmailLexer::S_HTAB => true,
EmailLexer::S_CR => true,
EmailLexer::S_LF => true
];
$setSpecialsWarning = true;
$this->lexer->moveNext();
while (((array) $this->lexer->token)['type'] !== EmailLexer::S_DQUOTE && null !== ((array) $this->lexer->token)['type']) {
if (isset($special[((array) $this->lexer->token)['type']]) && $setSpecialsWarning) {
$this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
$setSpecialsWarning = false;
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_BACKSLASH && $this->lexer->isNextToken(EmailLexer::S_DQUOTE)) {
$this->lexer->moveNext();
}
$this->lexer->moveNext();
if (!$this->escaped() && isset($invalid[((array) $this->lexer->token)['type']])) {
return new InvalidEmail(new ExpectingATEXT("Expecting ATEXT between DQUOTE"), ((array) $this->lexer->token)['value']);
}
}
$prev = $this->lexer->getPrevious();
if ($prev['type'] === EmailLexer::S_BACKSLASH) {
$validQuotedString = $this->checkDQUOTE();
if($validQuotedString->isInvalid()) return $validQuotedString;
}
if (!$this->lexer->isNextToken(EmailLexer::S_AT) && $prev['type'] !== EmailLexer::S_BACKSLASH) {
return new InvalidEmail(new ExpectingATEXT("Expecting ATEXT between DQUOTE"), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
protected function checkDQUOTE() : Result
{
$previous = $this->lexer->getPrevious();
if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type'] === EmailLexer::GENERIC) {
$description = 'https://tools.ietf.org/html/rfc5322#section-3.2.4 - quoted string should be a unit';
return new InvalidEmail(new ExpectingATEXT($description), ((array) $this->lexer->token)['value']);
}
try {
$this->lexer->find(EmailLexer::S_DQUOTE);
} catch (\Exception $e) {
return new InvalidEmail(new UnclosedQuotedString(), ((array) $this->lexer->token)['value']);
}
$this->warnings[QuotedString::CODE] = new QuotedString($previous['value'], ((array) $this->lexer->token)['value']);
return new ValidEmail();
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Warning\CFWSNearAt;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\CFWSWithFWS;
use Egulias\EmailValidator\Result\Reason\CRNoLF;
use Egulias\EmailValidator\Result\Reason\AtextAfterCFWS;
use Egulias\EmailValidator\Result\Reason\CRLFAtTheEnd;
use Egulias\EmailValidator\Result\Reason\CRLFX2;
use Egulias\EmailValidator\Result\Reason\ExpectingCTEXT;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
class FoldingWhiteSpace extends PartParser
{
public const FWS_TYPES = [
EmailLexer::S_SP,
EmailLexer::S_HTAB,
EmailLexer::S_CR,
EmailLexer::S_LF,
EmailLexer::CRLF
];
public function parse() : Result
{
if (!$this->isFWS()) {
return new ValidEmail();
}
$previous = $this->lexer->getPrevious();
$resultCRLF = $this->checkCRLFInFWS();
if ($resultCRLF->isInvalid()) {
return $resultCRLF;
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_CR) {
return new InvalidEmail(new CRNoLF(), ((array) $this->lexer->token)['value']);
}
if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type'] !== EmailLexer::S_AT) {
return new InvalidEmail(new AtextAfterCFWS(), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_LF || ((array) $this->lexer->token)['type'] === EmailLexer::C_NUL) {
return new InvalidEmail(new ExpectingCTEXT(), ((array) $this->lexer->token)['value']);
}
if ($this->lexer->isNextToken(EmailLexer::S_AT) || $previous['type'] === EmailLexer::S_AT) {
$this->warnings[CFWSNearAt::CODE] = new CFWSNearAt();
} else {
$this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
}
return new ValidEmail();
}
protected function checkCRLFInFWS() : Result
{
if (((array) $this->lexer->token)['type'] !== EmailLexer::CRLF) {
return new ValidEmail();
}
if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) {
return new InvalidEmail(new CRLFX2(), ((array) $this->lexer->token)['value']);
}
//this has no coverage. Condition is repeated from above one
if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) {
return new InvalidEmail(new CRLFAtTheEnd(), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
protected function isFWS() : bool
{
if ($this->escaped()) {
return false;
}
return in_array(((array) $this->lexer->token)['type'], self::FWS_TYPES);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\CommentsInIDRight;
class IDLeftPart extends LocalPart
{
protected function parseComments(): Result
{
return new InvalidEmail(new CommentsInIDRight(), ((array) $this->lexer->token)['value']);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
class IDRightPart extends DomainPart
{
protected function validateTokens(bool $hasComments) : Result
{
$invalidDomainTokens = [
EmailLexer::S_DQUOTE => true,
EmailLexer::S_SQUOTE => true,
EmailLexer::S_BACKTICK => true,
EmailLexer::S_SEMICOLON => true,
EmailLexer::S_GREATERTHAN => true,
EmailLexer::S_LOWERTHAN => true,
];
if (isset($invalidDomainTokens[((array) $this->lexer->token)['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . ((array) $this->lexer->token)['value']), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\LocalTooLong;
use Egulias\EmailValidator\Result\Reason\DotAtEnd;
use Egulias\EmailValidator\Result\Reason\DotAtStart;
use Egulias\EmailValidator\Result\Reason\ConsecutiveDot;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Parser\CommentStrategy\LocalComment;
class LocalPart extends PartParser
{
public const INVALID_TOKENS = [
EmailLexer::S_COMMA => EmailLexer::S_COMMA,
EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET,
EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET,
EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN,
EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN,
EmailLexer::S_COLON => EmailLexer::S_COLON,
EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON,
EmailLexer::INVALID => EmailLexer::INVALID
];
/**
* @var string
*/
private $localPart = '';
public function parse() : Result
{
$this->lexer->startRecording();
while (((array) $this->lexer->token)['type'] !== EmailLexer::S_AT && null !== ((array) $this->lexer->token)['type']) {
if ($this->hasDotAtStart()) {
return new InvalidEmail(new DotAtStart(), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_DQUOTE) {
$dquoteParsingResult = $this->parseDoubleQuote();
//Invalid double quote parsing
if($dquoteParsingResult->isInvalid()) {
return $dquoteParsingResult;
}
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_OPENPARENTHESIS ||
((array) $this->lexer->token)['type'] === EmailLexer::S_CLOSEPARENTHESIS ) {
$commentsResult = $this->parseComments();
//Invalid comment parsing
if($commentsResult->isInvalid()) {
return $commentsResult;
}
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
return new InvalidEmail(new ConsecutiveDot(), ((array) $this->lexer->token)['value']);
}
if (((array) $this->lexer->token)['type'] === EmailLexer::S_DOT &&
$this->lexer->isNextToken(EmailLexer::S_AT)
) {
return new InvalidEmail(new DotAtEnd(), ((array) $this->lexer->token)['value']);
}
$resultEscaping = $this->validateEscaping();
if ($resultEscaping->isInvalid()) {
return $resultEscaping;
}
$resultToken = $this->validateTokens(false);
if ($resultToken->isInvalid()) {
return $resultToken;
}
$resultFWS = $this->parseLocalFWS();
if($resultFWS->isInvalid()) {
return $resultFWS;
}
$this->lexer->moveNext();
}
$this->lexer->stopRecording();
$this->localPart = rtrim($this->lexer->getAccumulatedValues(), '@');
if (strlen($this->localPart) > LocalTooLong::LOCAL_PART_LENGTH) {
$this->warnings[LocalTooLong::CODE] = new LocalTooLong();
}
return new ValidEmail();
}
protected function validateTokens(bool $hasComments) : Result
{
if (isset(self::INVALID_TOKENS[((array) $this->lexer->token)['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token found'), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
public function localPart() : string
{
return $this->localPart;
}
private function parseLocalFWS() : Result
{
$foldingWS = new FoldingWhiteSpace($this->lexer);
$resultFWS = $foldingWS->parse();
if ($resultFWS->isValid()) {
$this->warnings = array_merge($this->warnings, $foldingWS->getWarnings());
}
return $resultFWS;
}
private function hasDotAtStart() : bool
{
return ((array) $this->lexer->token)['type'] === EmailLexer::S_DOT && null === $this->lexer->getPrevious()['type'];
}
private function parseDoubleQuote() : Result
{
$dquoteParser = new DoubleQuote($this->lexer);
$parseAgain = $dquoteParser->parse();
$this->warnings = array_merge($this->warnings, $dquoteParser->getWarnings());
return $parseAgain;
}
protected function parseComments(): Result
{
$commentParser = new Comment($this->lexer, new LocalComment());
$result = $commentParser->parse();
$this->warnings = array_merge($this->warnings, $commentParser->getWarnings());
if($result->isInvalid()) {
return $result;
}
return $result;
}
private function validateEscaping() : Result
{
//Backslash found
if (((array) $this->lexer->token)['type'] !== EmailLexer::S_BACKSLASH) {
return new ValidEmail();
}
if ($this->lexer->isNextToken(EmailLexer::GENERIC)) {
return new InvalidEmail(new ExpectingATEXT('Found ATOM after escaping'), ((array) $this->lexer->token)['value']);
}
if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) {
return new ValidEmail();
}
return new ValidEmail();
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ConsecutiveDot;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
abstract class PartParser
{
/**
* @var array
*/
protected $warnings = [];
/**
* @var EmailLexer
*/
protected $lexer;
public function __construct(EmailLexer $lexer)
{
$this->lexer = $lexer;
}
abstract public function parse() : Result;
/**
* @return \Egulias\EmailValidator\Warning\Warning[]
*/
public function getWarnings()
{
return $this->warnings;
}
protected function parseFWS() : Result
{
$foldingWS = new FoldingWhiteSpace($this->lexer);
$resultFWS = $foldingWS->parse();
$this->warnings = array_merge($this->warnings, $foldingWS->getWarnings());
return $resultFWS;
}
protected function checkConsecutiveDots() : Result
{
if (((array) $this->lexer->token)['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
return new InvalidEmail(new ConsecutiveDot(), ((array) $this->lexer->token)['value']);
}
return new ValidEmail();
}
protected function escaped() : bool
{
$previous = $this->lexer->getPrevious();
return $previous && $previous['type'] === EmailLexer::S_BACKSLASH
&&
((array) $this->lexer->token)['type'] !== EmailLexer::GENERIC;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Egulias\EmailValidator\Result;
use Egulias\EmailValidator\Result\Reason\Reason;
class InvalidEmail implements Result
{
private $token;
/**
* @var Reason
*/
protected $reason;
public function __construct(Reason $reason, string $token)
{
$this->token = $token;
$this->reason = $reason;
}
public function isValid(): bool
{
return false;
}
public function isInvalid(): bool
{
return true;
}
public function description(): string
{
return $this->reason->description() . " in char " . $this->token;
}
public function code(): int
{
return $this->reason->code();
}
public function reason() : Reason
{
return $this->reason;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Egulias\EmailValidator\Result;
use Egulias\EmailValidator\Result\Reason\EmptyReason;
use Egulias\EmailValidator\Result\Reason\Reason;
/**
* @psalm-suppress PropertyNotSetInConstructor
*/
class MultipleErrors extends InvalidEmail
{
/**
* @var Reason[]
*/
private $reasons = [];
public function __construct()
{
}
public function addReason(Reason $reason) : void
{
$this->reasons[$reason->code()] = $reason;
}
/**
* @return Reason[]
*/
public function getReasons() : array
{
return $this->reasons;
}
public function reason() : Reason
{
return 0 !== count($this->reasons)
? current($this->reasons)
: new EmptyReason();
}
public function description() : string
{
$description = '';
foreach($this->reasons as $reason) {
$description .= $reason->description() . PHP_EOL;
}
return $description;
}
public function code() : int
{
return 0;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class AtextAfterCFWS implements Reason
{
public function code() : int
{
return 133;
}
public function description() : string
{
return 'ATEXT found after CFWS';
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class CRLFAtTheEnd implements Reason
{
public const CODE = 149;
public const REASON = "CRLF at the end";
public function code() : int
{
return 149;
}
public function description() : string
{
return 'CRLF at the end';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class CRLFX2 implements Reason
{
public function code() : int
{
return 148;
}
public function description() : string
{
return 'CR LF tokens found twice';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class CRNoLF implements Reason
{
public function code() : int
{
return 150;
}
public function description() : string
{
return 'Missing LF after CR';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class CharNotAllowed implements Reason
{
public function code() : int
{
return 1;
}
public function description() : string
{
return "Character not allowed";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class CommaInDomain implements Reason
{
public function code() : int
{
return 200;
}
public function description() : string
{
return "Comma ',' is not allowed in domain part";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class CommentsInIDRight implements Reason
{
public function code() : int
{
return 400;
}
public function description() : string
{
return 'Comments are not allowed in IDRight for message-id';
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class ConsecutiveAt implements Reason
{
public function code() : int
{
return 128;
}
public function description() : string
{
return '@ found after another @';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class ConsecutiveDot implements Reason
{
public function code() : int
{
return 132;
}
public function description() : string
{
return 'Concecutive DOT found';
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
abstract class DetailedReason implements Reason
{
protected $detailedDescription;
public function __construct(string $details)
{
$this->detailedDescription = $details;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class DomainAcceptsNoMail implements Reason
{
public function code() : int
{
return 154;
}
public function description() : string
{
return 'Domain accepts no mail (Null MX, RFC7505)';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class DomainHyphened extends DetailedReason
{
public function code() : int
{
return 144;
}
public function description() : string
{
return 'S_HYPHEN found in domain';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class DomainTooLong implements Reason
{
public function code() : int
{
return 244;
}
public function description() : string
{
return 'Domain is longer than 253 characters';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class DotAtEnd implements Reason
{
public function code() : int
{
return 142;
}
public function description() : string
{
return 'Dot at the end';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class DotAtStart implements Reason
{
public function code() : int
{
return 141;
}
public function description() : string
{
return "Starts with a DOT";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class EmptyReason implements Reason
{
public function code() : int
{
return 0;
}
public function description() : string
{
return 'Empty reason';
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class ExceptionFound implements Reason
{
/**
* @var \Exception
*/
private $exception;
public function __construct(\Exception $exception)
{
$this->exception = $exception;
}
public function code() : int
{
return 999;
}
public function description() : string
{
return $this->exception->getMessage();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class ExpectingATEXT extends DetailedReason
{
public function code() : int
{
return 137;
}
public function description() : string
{
return "Expecting ATEXT (Printable US-ASCII). Extended: " . $this->detailedDescription;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class ExpectingCTEXT implements Reason
{
public function code() : int
{
return 139;
}
public function description() : string
{
return 'Expecting CTEXT';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class ExpectingDTEXT implements Reason
{
public function code() : int
{
return 129;
}
public function description() : string
{
return 'Expecting DTEXT';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class ExpectingDomainLiteralClose implements Reason
{
public function code() : int
{
return 137;
}
public function description() : string
{
return "Closing bracket ']' for domain literal not found";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class LabelTooLong implements Reason
{
public function code() : int
{
return 245;
}
public function description() : string
{
return 'Domain "label" is longer than 63 characters';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class LocalOrReservedDomain implements Reason
{
public function code() : int
{
return 153;
}
public function description() : string
{
return 'Local, mDNS or reserved domain (RFC2606, RFC6762)';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class NoDNSRecord implements Reason
{
public function code() : int
{
return 5;
}
public function description() : string
{
return 'No MX or A DSN record was found for this email';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class NoDomainPart implements Reason
{
public function code() : int
{
return 131;
}
public function description() : string
{
return 'No domain part found';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class NoLocalPart implements Reason
{
public function code() : int
{
return 130;
}
public function description() : string
{
return "No local part";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class RFCWarnings implements Reason
{
public function code() : int
{
return 997;
}
public function description() : string
{
return 'Warnings found after validating';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
interface Reason
{
/**
* Code for user land to act upon;
*/
public function code() : int;
/**
* Short description of the result, human readable.
*/
public function description() : string;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class SpoofEmail implements Reason
{
public function code() : int
{
return 298;
}
public function description() : string
{
return 'The email contains mixed UTF8 chars that makes it suspicious';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class UnOpenedComment implements Reason
{
public function code() : int
{
return 152;
}
public function description(): string
{
return 'Missing opening comment parentheses - https://tools.ietf.org/html/rfc5322#section-3.2.2';
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
/**
* Used on SERVFAIL, TIMEOUT or other runtime and network errors
*/
class UnableToGetDNSRecord extends NoDNSRecord
{
public function code() : int
{
return 3;
}
public function description() : string
{
return 'Unable to get DNS records for the host';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class UnclosedComment implements Reason
{
public function code() : int
{
return 146;
}
public function description(): string
{
return 'No closing comment token found';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class UnclosedQuotedString implements Reason
{
public function code() : int
{
return 145;
}
public function description() : string
{
return "Unclosed quoted string";
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class UnusualElements implements Reason
{
/**
* @var string $element
*/
private $element = '';
public function __construct(string $element)
{
$this->element = $element;
}
public function code() : int
{
return 201;
}
public function description() : string
{
return 'Unusual element found, wourld render invalid in majority of cases. Element found: ' . $this->element;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Egulias\EmailValidator\Result;
interface Result
{
/**
* Is validation result valid?
*/
public function isValid() : bool;
/**
* Is validation result invalid?
* Usually the inverse of isValid()
*/
public function isInvalid() : bool;
/**
* Short description of the result, human readable.
*/
public function description() : string;
/**
* Code for user land to act upon.
*/
public function code() : int;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Egulias\EmailValidator\Result;
use Egulias\EmailValidator\Result\Reason\SpoofEmail as ReasonSpoofEmail;
class SpoofEmail extends InvalidEmail
{
public function __construct()
{
$this->reason = new ReasonSpoofEmail();
parent::__construct($this->reason, '');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Egulias\EmailValidator\Result;
class ValidEmail implements Result
{
public function isValid(): bool
{
return true;
}
public function isInvalid(): bool
{
return false;
}
public function description(): string
{
return "Valid email";
}
public function code(): int
{
return 0;
}
}

Some files were not shown because too many files have changed in this diff Show More