diff --git a/.gitignore b/.gitignore
index a67f91e..10ed1d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,9 @@
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
+/public/attachments/
+/migrations/
+/data/
/var/
/vendor/
###< symfony/framework-bundle ###
diff --git a/.php-version b/.php-version
new file mode 100644
index 0000000..37722eb
--- /dev/null
+++ b/.php-version
@@ -0,0 +1 @@
+7.4
diff --git a/composer.json b/composer.json
index e88bf98..d0005e3 100644
--- a/composer.json
+++ b/composer.json
@@ -13,11 +13,16 @@
"doctrine/orm": "^2.7",
"php-mime-mail-parser/php-mime-mail-parser": "^6.0",
"ramsey/uuid-doctrine": "^1.6",
+ "sensio/framework-extra-bundle": "^5.6",
+ "symfony/apache-pack": "^1.0",
+ "symfony/asset": "5.2.*",
"symfony/console": "5.2.*",
"symfony/dotenv": "5.2.*",
+ "symfony/filesystem": "5.2.*",
"symfony/flex": "^1.3.1",
"symfony/framework-bundle": "5.2.*",
"symfony/maker-bundle": "^1.23",
+ "symfony/twig-bundle": "5.2.*",
"symfony/yaml": "5.2.*"
},
"require-dev": {
diff --git a/config/bundles.php b/config/bundles.php
index de8898b..9e1e12f 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -5,4 +5,6 @@ return [
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
+ Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
+ Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
];
diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml
index 5e80e77..8b9501c 100644
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -4,6 +4,7 @@ doctrine:
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
+ charset: UTF8
#server_version: '5.7'
orm:
auto_generate_proxy_classes: true
diff --git a/config/packages/sensio_framework_extra.yaml b/config/packages/sensio_framework_extra.yaml
new file mode 100644
index 0000000..1821ccc
--- /dev/null
+++ b/config/packages/sensio_framework_extra.yaml
@@ -0,0 +1,3 @@
+sensio_framework_extra:
+ router:
+ annotations: false
diff --git a/config/packages/test/twig.yaml b/config/packages/test/twig.yaml
new file mode 100644
index 0000000..8c6e0b4
--- /dev/null
+++ b/config/packages/test/twig.yaml
@@ -0,0 +1,2 @@
+twig:
+ strict_variables: true
diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml
new file mode 100644
index 0000000..b3cdf30
--- /dev/null
+++ b/config/packages/twig.yaml
@@ -0,0 +1,2 @@
+twig:
+ default_path: '%kernel.project_dir%/templates'
diff --git a/config/routes.yaml b/config/routes.yaml
index c3283aa..fdfc577 100644
--- a/config/routes.yaml
+++ b/config/routes.yaml
@@ -1,3 +1,7 @@
+controllers:
+ resource: '../src/Controller/'
+ type: annotation
+
#index:
# path: /
# controller: App\Controller\DefaultController::index
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..2776637
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,66 @@
+# Use the front controller as index file. It serves as a fallback solution when
+# every other rewrite/redirect fails (e.g. in an aliased environment without
+# mod_rewrite). Additionally, this reduces the matching process for the
+# start page (path "/") because otherwise Apache will apply the rewriting rules
+# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
+DirectoryIndex index.php
+
+# By default, Apache does not evaluate symbolic links if you did not enable this
+# feature in your server configuration. Uncomment the following line if you
+# install assets as symlinks or if you experience problems related to symlinks
+# when compiling LESS/Sass/CoffeScript assets.
+# Options +FollowSymlinks
+
+# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
+# to the front controller "/index.php" but be rewritten to "/index.php/index".
+
+ Options -MultiViews
+
+
+
+ RewriteEngine On
+
+ # Determine the RewriteBase automatically and set it as environment variable.
+ # If you are using Apache aliases to do mass virtual hosting or installed the
+ # project in a subdirectory, the base path will be prepended to allow proper
+ # resolution of the index.php file and to redirect to the correct URI. It will
+ # work in environments without path prefix as well, providing a safe, one-size
+ # fits all solution. But as you do not need it in this case, you can comment
+ # the following 2 lines to eliminate the overhead.
+ RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
+ RewriteRule .* - [E=BASE:%1]
+
+ # Sets the HTTP_AUTHORIZATION header removed by Apache
+ RewriteCond %{HTTP:Authorization} .+
+ RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
+
+ # Redirect to URI without front controller to prevent duplicate content
+ # (with and without `/index.php`). Only do this redirect on the initial
+ # rewrite by Apache and not on subsequent cycles. Otherwise we would get an
+ # endless redirect loop (request -> rewrite to front controller ->
+ # redirect -> request -> ...).
+ # So in case you get a "too many redirects" error or you always get redirected
+ # to the start page because your Apache does not expose the REDIRECT_STATUS
+ # environment variable, you have 2 choices:
+ # - disable this feature by commenting the following 2 lines or
+ # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
+ # following RewriteCond (best solution)
+ RewriteCond %{ENV:REDIRECT_STATUS} =""
+ RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
+
+ # If the requested filename exists, simply serve it.
+ # We only want to let Apache serve files and not directories.
+ # Rewrite all other queries to the front controller.
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^ %{ENV:BASE}/index.php [L]
+
+
+
+
+ # When mod_rewrite is not available, we instruct a temporary redirect of
+ # the start page to the front controller explicitly so that the website
+ # and the generated links can still be used.
+ RedirectMatch 307 ^/$ /index.php/
+ # RedirectTemp cannot be used instead
+
+
diff --git a/src/Command/MailImportCommand.php b/src/Command/MailImportCommand.php
new file mode 100644
index 0000000..5b2de39
--- /dev/null
+++ b/src/Command/MailImportCommand.php
@@ -0,0 +1,117 @@
+em = $em;
+ $this->mailingRepo = $mailingRepo;
+ $this->kernel = $kernel;
+ }
+
+ protected function configure()
+ {
+ $this
+ ->setDescription('Import a mail into a mailing')
+ ->addArgument('mailing_id', InputArgument::OPTIONAL, 'ID of the mailing')
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $mailingId = $input->getArgument('mailing_id');
+
+ $mailing = $this->mailingRepo->find(['id' => $mailingId]);
+
+ if (null === $mailing) {
+ $io->error(sprintf('Mailing "%s" is not found!', $mailingId));
+
+ return Command::FAILURE;
+ }
+
+ $stdIn = file_get_contents('php://stdin');
+
+ if (empty($stdIn)) {
+ $io->error('Standard input is empty');
+
+ return Command::FAILURE;
+ }
+
+ $parser = new Parser();
+ $parser->setText($stdIn);
+
+ $subject = $parser->getHeader('subject');
+ $date = $parser->getHeader('date');
+ $text = $parser->getMessageBody('text');
+ $htmlEmbeddedContent = $parser->getMessageBody('htmlEmbedded');
+ $attachments = $parser->getAttachments();
+
+ if ($subject === false && $date === false) {
+ $io->error('The subject and the date are empty. Is it a valid mail?');
+
+ return Command::FAILURE;
+ }
+
+ $entity = new Mail();
+ $entity
+ ->setMailing($mailing)
+ ->setSubject($subject)
+ ->setDate(new \DateTime($date))
+ ->setTextContent($text)
+ ->setHtmlContent($htmlEmbeddedContent);
+
+ $this->em->persist($entity);
+ $this->em->flush();
+
+ if (!empty($attachments)) {
+ $attachmentsDirectory = $this->kernel->getProjectDir().'/public/attachments/'.$mailing->getId().'/'.$entity->getId();
+
+ $filesystem = new Filesystem();
+ $filesystem->mkdir($attachmentsDirectory);
+
+ foreach ($attachments as $attachment) {
+ $filename = basename($attachment->save($attachmentsDirectory, Parser::ATTACHMENT_DUPLICATE_SUFFIX));
+
+ $mailAttachment = new MailAttachment();
+ $mailAttachment
+ ->setMail($entity)
+ ->setContentType($attachment->getContentType())
+ ->setFilename($filename);
+
+ $this->em->persist($mailAttachment);
+ $this->em->flush();
+ }
+ }
+
+ $io->success('Mail imported!');
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/Controller/MailController.php b/src/Controller/MailController.php
new file mode 100644
index 0000000..518674e
--- /dev/null
+++ b/src/Controller/MailController.php
@@ -0,0 +1,58 @@
+getMailing()->getId() !== $mailing) {
+ throw $this->createNotFoundException();
+ }
+
+ return $this->render('mail/show.html.twig', [
+ 'mail' => $mail,
+ ]);
+ }
+
+ /**
+ * @Route("/mail/{mailing}/{id}/html", name="mail_html")
+ */
+ public function html(string $mailing, Mail $mail): Response
+ {
+ if ($mail->getMailing()->getId() !== $mailing) {
+ throw $this->createNotFoundException();
+ }
+
+ return $this->render('mail/html.html.twig', [
+ 'mail' => $mail,
+ ]);
+ }
+
+ /**
+ * @Route("/mail/{mailing}/{id}/text", name="mail_text")
+ */
+ public function text(string $mailing, Mail $mail): Response
+ {
+ if ($mail->getMailing()->getId() !== $mailing) {
+ throw $this->createNotFoundException();
+ }
+
+ $response = $this->render('mail/text.html.twig', [
+ 'mail' => $mail,
+ ]);
+
+ $response->headers->set('Content-Type', 'text/plain');
+
+ return $response;
+ }
+}
diff --git a/src/Controller/MailingController.php b/src/Controller/MailingController.php
new file mode 100644
index 0000000..1cf80d3
--- /dev/null
+++ b/src/Controller/MailingController.php
@@ -0,0 +1,38 @@
+findAll(
+ [
+ 'mailing' => $mailing->getId(),
+ ],
+ [
+ 'date' => 'DESC',
+ ],
+ 20
+ );
+
+ $response = $this->render('mailing/rss.html.twig', [
+ 'mailing' => $mailing,
+ 'mails' => $mails,
+ ]);
+
+ $response->headers->set('Content-Type', 'application/rss+xml');
+
+ return $response;
+ }
+}
diff --git a/src/Doctrine/Timestampable.php b/src/Doctrine/Timestampable.php
index 74fa338..f8ed9df 100644
--- a/src/Doctrine/Timestampable.php
+++ b/src/Doctrine/Timestampable.php
@@ -51,8 +51,7 @@ trait Timestampable
return $this->createdAt;
}
-
- public function setUpdatedAt(?DateTime $updatedAt): self
+ public function setUpdatedAt(?\DateTime $updatedAt): self
{
$this->updatedAt = $updatedAt;
diff --git a/src/Entity/Mail.php b/src/Entity/Mail.php
index 4f3806a..2986d24 100644
--- a/src/Entity/Mail.php
+++ b/src/Entity/Mail.php
@@ -3,17 +3,24 @@
namespace App\Entity;
use App\Repository\MailRepository;
+use App\Doctrine\Timestampable;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=MailRepository::class)
+ * @ORM\HasLifecycleCallbacks
*/
class Mail
{
+ use Timestampable;
+
/**
- * @ORM\Id
- * @ORM\GeneratedValue
- * @ORM\Column(type="integer")
+ * @ORM\Id()
+ * @ORM\GeneratedValue(strategy="CUSTOM")
+ * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
+ * @ORM\Column(type="uuid")
*/
private $id;
@@ -29,7 +36,7 @@ class Mail
private $subject;
/**
- * @ORM\Column(type="date")
+ * @ORM\Column(type="datetime")
*/
private $date;
@@ -43,7 +50,17 @@ class Mail
*/
private $textContent;
- public function getId(): ?int
+ /**
+ * @ORM\OneToMany(targetEntity=MailAttachment::class, mappedBy="mail", orphanRemoval=true)
+ */
+ private $mailAttachments;
+
+ public function __construct()
+ {
+ $this->mailAttachments = new ArrayCollection();
+ }
+
+ public function getId(): ?string
{
return $this->id;
}
@@ -107,4 +124,34 @@ class Mail
return $this;
}
+
+ /**
+ * @return Collection|MailAttachment[]
+ */
+ public function getMailAttachments(): Collection
+ {
+ return $this->mailAttachments;
+ }
+
+ public function addMailAttachment(MailAttachment $mailAttachment): self
+ {
+ if (!$this->mailAttachments->contains($mailAttachment)) {
+ $this->mailAttachments[] = $mailAttachment;
+ $mailAttachment->setMail($this);
+ }
+
+ return $this;
+ }
+
+ public function removeMailAttachment(MailAttachment $mailAttachment): self
+ {
+ if ($this->mailAttachments->removeElement($mailAttachment)) {
+ // set the owning side to null (unless already changed)
+ if ($mailAttachment->getMail() === $this) {
+ $mailAttachment->setMail(null);
+ }
+ }
+
+ return $this;
+ }
}
diff --git a/src/Entity/MailAttachment.php b/src/Entity/MailAttachment.php
new file mode 100644
index 0000000..4554919
--- /dev/null
+++ b/src/Entity/MailAttachment.php
@@ -0,0 +1,76 @@
+id;
+ }
+
+ public function getMail(): ?Mail
+ {
+ return $this->mail;
+ }
+
+ public function setMail(?Mail $mail): self
+ {
+ $this->mail = $mail;
+
+ return $this;
+ }
+
+ public function getFilename(): ?string
+ {
+ return $this->filename;
+ }
+
+ public function setFilename(string $filename): self
+ {
+ $this->filename = $filename;
+
+ return $this;
+ }
+
+ public function getContentType(): ?string
+ {
+ return $this->contentType;
+ }
+
+ public function setContentType(string $contentType): self
+ {
+ $this->contentType = $contentType;
+
+ return $this;
+ }
+}
diff --git a/src/Repository/MailAttachmentRepository.php b/src/Repository/MailAttachmentRepository.php
new file mode 100644
index 0000000..e74fedc
--- /dev/null
+++ b/src/Repository/MailAttachmentRepository.php
@@ -0,0 +1,50 @@
+createQueryBuilder('m')
+ ->andWhere('m.exampleField = :val')
+ ->setParameter('val', $value)
+ ->orderBy('m.id', 'ASC')
+ ->setMaxResults(10)
+ ->getQuery()
+ ->getResult()
+ ;
+ }
+ */
+
+ /*
+ public function findOneBySomeField($value): ?MailAttachment
+ {
+ return $this->createQueryBuilder('m')
+ ->andWhere('m.exampleField = :val')
+ ->setParameter('val', $value)
+ ->getQuery()
+ ->getOneOrNullResult()
+ ;
+ }
+ */
+}
diff --git a/symfony.lock b/symfony.lock
index 12ae381..70de822 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -132,6 +132,33 @@
"config/packages/ramsey_uuid_doctrine.yaml"
]
},
+ "sensio/framework-extra-bundle": {
+ "version": "5.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "5.2",
+ "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
+ },
+ "files": [
+ "config/packages/sensio_framework_extra.yaml"
+ ]
+ },
+ "symfony/apache-pack": {
+ "version": "1.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes-contrib",
+ "branch": "master",
+ "version": "1.0",
+ "ref": "71599f5b0fdeeeec0fb90e9b17c85e6f5e1350c1"
+ },
+ "files": [
+ "public/.htaccess"
+ ]
+ },
+ "symfony/asset": {
+ "version": "v5.2.0-rc1"
+ },
"symfony/cache": {
"version": "v5.2.0-rc1"
},
@@ -271,6 +298,26 @@
"symfony/string": {
"version": "v5.2.0-rc1"
},
+ "symfony/translation-contracts": {
+ "version": "v2.3.0"
+ },
+ "symfony/twig-bridge": {
+ "version": "v5.2.0-rc1"
+ },
+ "symfony/twig-bundle": {
+ "version": "5.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "5.0",
+ "ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d"
+ },
+ "files": [
+ "config/packages/test/twig.yaml",
+ "config/packages/twig.yaml",
+ "templates/base.html.twig"
+ ]
+ },
"symfony/var-dumper": {
"version": "v5.2.0-rc1"
},
@@ -280,6 +327,9 @@
"symfony/yaml": {
"version": "v5.2.0-rc1"
},
+ "twig/twig": {
+ "version": "v3.1.1"
+ },
"webimpress/safe-writer": {
"version": "2.1.0"
}
diff --git a/templates/base.html.twig b/templates/base.html.twig
new file mode 100644
index 0000000..043f42d
--- /dev/null
+++ b/templates/base.html.twig
@@ -0,0 +1,12 @@
+
+
+
+
+ {% block title %}Welcome!{% endblock %}
+ {% block stylesheets %}{% endblock %}
+
+
+ {% block body %}{% endblock %}
+ {% block javascripts %}{% endblock %}
+
+
diff --git a/templates/mail/html.html.twig b/templates/mail/html.html.twig
new file mode 100644
index 0000000..0ca8faa
--- /dev/null
+++ b/templates/mail/html.html.twig
@@ -0,0 +1 @@
+{{ mail.htmlContent|raw }}
diff --git a/templates/mail/show.html.twig b/templates/mail/show.html.twig
new file mode 100644
index 0000000..d849959
--- /dev/null
+++ b/templates/mail/show.html.twig
@@ -0,0 +1,87 @@
+
+
+
+
+ {{ mail.subject }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if mail.mailAttachments|length %}
+
+ {% for item in mail.mailAttachments %}
+ -
+
+ {{ item.filename }}
+
+
+
+ {{ item.contentType }}
+
+
+ {% endfor %}
+
+ {% else %}
+
+ Aucune pièce jointe
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/mail/text.html.twig b/templates/mail/text.html.twig
new file mode 100644
index 0000000..95a40f3
--- /dev/null
+++ b/templates/mail/text.html.twig
@@ -0,0 +1 @@
+{{ mail.textContent|raw }}
diff --git a/templates/mailing/rss.html.twig b/templates/mailing/rss.html.twig
new file mode 100644
index 0000000..9efc47d
--- /dev/null
+++ b/templates/mailing/rss.html.twig
@@ -0,0 +1,24 @@
+
+
+
+ {{ mailing.label }}
+
+ {% for item in mails %}
+ -
+
+ {{ absolute_url(path('mail_show', {mailing: mailing.id, id: item.id})) }}
+
+
+ {{ item.id }}
+ {{ item.date|date('r') }}
+
+ {% endfor %}
+
+
+