add importation mails and views

This commit is contained in:
Simon Vieille 2020-11-11 16:37:07 +01:00
parent 933a7116c6
commit 331a8f8b09
Signed by: deblan
GPG Key ID: 03383D15A1D31745
23 changed files with 656 additions and 7 deletions

3
.gitignore vendored
View File

@ -5,6 +5,9 @@
/.env.*.local /.env.*.local
/config/secrets/prod/prod.decrypt.private.php /config/secrets/prod/prod.decrypt.private.php
/public/bundles/ /public/bundles/
/public/attachments/
/migrations/
/data/
/var/ /var/
/vendor/ /vendor/
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###

1
.php-version Normal file
View File

@ -0,0 +1 @@
7.4

View File

@ -13,11 +13,16 @@
"doctrine/orm": "^2.7", "doctrine/orm": "^2.7",
"php-mime-mail-parser/php-mime-mail-parser": "^6.0", "php-mime-mail-parser/php-mime-mail-parser": "^6.0",
"ramsey/uuid-doctrine": "^1.6", "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/console": "5.2.*",
"symfony/dotenv": "5.2.*", "symfony/dotenv": "5.2.*",
"symfony/filesystem": "5.2.*",
"symfony/flex": "^1.3.1", "symfony/flex": "^1.3.1",
"symfony/framework-bundle": "5.2.*", "symfony/framework-bundle": "5.2.*",
"symfony/maker-bundle": "^1.23", "symfony/maker-bundle": "^1.23",
"symfony/twig-bundle": "5.2.*",
"symfony/yaml": "5.2.*" "symfony/yaml": "5.2.*"
}, },
"require-dev": { "require-dev": {

View File

@ -5,4 +5,6 @@ return [
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
]; ];

View File

@ -4,6 +4,7 @@ doctrine:
# IMPORTANT: You MUST configure your server version, # IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file) # either here or in the DATABASE_URL env var (see .env file)
charset: UTF8
#server_version: '5.7' #server_version: '5.7'
orm: orm:
auto_generate_proxy_classes: true auto_generate_proxy_classes: true

View File

@ -0,0 +1,3 @@
sensio_framework_extra:
router:
annotations: false

View File

@ -0,0 +1,2 @@
twig:
strict_variables: true

View File

@ -0,0 +1,2 @@
twig:
default_path: '%kernel.project_dir%/templates'

View File

@ -1,3 +1,7 @@
controllers:
resource: '../src/Controller/'
type: annotation
#index: #index:
# path: / # path: /
# controller: App\Controller\DefaultController::index # controller: App\Controller\DefaultController::index

66
public/.htaccess Normal file
View File

@ -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".
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
<IfModule mod_rewrite.c>
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]
</IfModule>
<IfModule !mod_rewrite.c>
<IfModule mod_alias.c>
# 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
</IfModule>
</IfModule>

View File

@ -0,0 +1,117 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Repository\MailingRepository;
use Doctrine\ORM\EntityManagerInterface;
use PhpMimeMailParser\Parser;
use App\Entity\Mail;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Filesystem\Filesystem;
use App\Entity\MailAttachment;
class MailImportCommand extends Command
{
protected static $defaultName = 'mail:import';
protected EntityManagerInterface $em;
protected MailingRepository $mailingRepo;
protected KernelInterface $kernel;
public function __construct(EntityManagerInterface $em, MailingRepository $mailingRepo, KernelInterface $kernel)
{
parent::__construct();
$this->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;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Controller;
use App\Entity\Mail;
use App\Entity\Mailing;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class MailController extends AbstractController
{
/**
* @Route("/mail/{mailing}/{id}/show", name="mail_show")
*/
public function show(string $mailing, Mail $mail): Response
{
if ($mail->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;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\MailRepository;
use App\Entity\Mailing;
use App\Entity\Mail;
class MailingController extends AbstractController
{
/**
* @Route("/mailing/{id}/rss", name="mailing_rss")
*/
public function rss(Mailing $mailing, MailRepository $mailRepository): Response
{
$mails = $mailRepository->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;
}
}

View File

@ -51,8 +51,7 @@ trait Timestampable
return $this->createdAt; return $this->createdAt;
} }
public function setUpdatedAt(?\DateTime $updatedAt): self
public function setUpdatedAt(?DateTime $updatedAt): self
{ {
$this->updatedAt = $updatedAt; $this->updatedAt = $updatedAt;

View File

@ -3,17 +3,24 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\MailRepository; use App\Repository\MailRepository;
use App\Doctrine\Timestampable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** /**
* @ORM\Entity(repositoryClass=MailRepository::class) * @ORM\Entity(repositoryClass=MailRepository::class)
* @ORM\HasLifecycleCallbacks
*/ */
class Mail class Mail
{ {
use Timestampable;
/** /**
* @ORM\Id * @ORM\Id()
* @ORM\GeneratedValue * @ORM\GeneratedValue(strategy="CUSTOM")
* @ORM\Column(type="integer") * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
* @ORM\Column(type="uuid")
*/ */
private $id; private $id;
@ -29,7 +36,7 @@ class Mail
private $subject; private $subject;
/** /**
* @ORM\Column(type="date") * @ORM\Column(type="datetime")
*/ */
private $date; private $date;
@ -43,7 +50,17 @@ class Mail
*/ */
private $textContent; 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; return $this->id;
} }
@ -107,4 +124,34 @@ class Mail
return $this; 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;
}
} }

View File

@ -0,0 +1,76 @@
<?php
namespace App\Entity;
use App\Repository\MailAttachmentRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=MailAttachmentRepository::class)
*/
class MailAttachment
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Mail::class, inversedBy="mailAttachments")
* @ORM\JoinColumn(nullable=false)
*/
private $mail;
/**
* @ORM\Column(type="string", length=255)
*/
private $filename;
/**
* @ORM\Column(type="string", length=255)
*/
private $contentType;
public function getId(): ?int
{
return $this->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;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Repository;
use App\Entity\MailAttachment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method MailAttachment|null find($id, $lockMode = null, $lockVersion = null)
* @method MailAttachment|null findOneBy(array $criteria, array $orderBy = null)
* @method MailAttachment[] findAll()
* @method MailAttachment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class MailAttachmentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MailAttachment::class);
}
// /**
// * @return MailAttachment[] Returns an array of MailAttachment objects
// */
/*
public function findByExampleField($value)
{
return $this->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()
;
}
*/
}

View File

@ -132,6 +132,33 @@
"config/packages/ramsey_uuid_doctrine.yaml" "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": { "symfony/cache": {
"version": "v5.2.0-rc1" "version": "v5.2.0-rc1"
}, },
@ -271,6 +298,26 @@
"symfony/string": { "symfony/string": {
"version": "v5.2.0-rc1" "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": { "symfony/var-dumper": {
"version": "v5.2.0-rc1" "version": "v5.2.0-rc1"
}, },
@ -280,6 +327,9 @@
"symfony/yaml": { "symfony/yaml": {
"version": "v5.2.0-rc1" "version": "v5.2.0-rc1"
}, },
"twig/twig": {
"version": "v3.1.1"
},
"webimpress/safe-writer": { "webimpress/safe-writer": {
"version": "2.1.0" "version": "2.1.0"
} }

12
templates/base.html.twig Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1 @@
{{ mail.htmlContent|raw }}

View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ mail.subject }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<style>
body {
margin: 0;
padding: 10px 0 0 0;
}
.col-12 {
padding: 0;
}
.nav-tabs {
padding-left: 20px;
}
iframe {
width: 100%;
height: calc(100vh - 50px);
border: 0;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<ul class="nav nav-tabs" id="tabs">
<li class="nav-item">
<a class="nav-link active" id="html-tab" data-toggle="tab" href="#html" role="tab" aria-controls="html" aria-selected="true">
HTML
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="text-tab" data-toggle="tab" href="#text" role="tab" aria-controls="text" aria-selected="true">
Texte
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="attachments-tab" data-toggle="tab" href="#attachments" role="tab" aria-controls="text" aria-selected="true">
Pièces jointes
</a>
</li>
</ul>
</div>
<div class="col-12">
<div class="tab-content">
<div class="tab-pane fade show active" id="html" role="tabpanel" aria-labelledby="html-tab">
<iframe src="{{ path('mail_html', {mailing: mail.mailing.id, id: mail.id}) }}"></iframe>
</div>
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
<iframe src="{{ path('mail_text', {mailing: mail.mailing.id, id: mail.id}) }}"></iframe>
</div>
<div class="tab-pane fade" id="attachments" role="tabpanel" aria-labelledby="attachments-tab">
{% if mail.mailAttachments|length %}
<ul>
{% for item in mail.mailAttachments %}
<li>
<a target="_blank" href="{{ asset('attachments/' ~ mail.mailing.id ~ '/' ~ mail.id ~ '/' ~ item.filename) }}">
{{ item.filename }}
</a>
<span class="badge badge-secondary">
{{ item.contentType }}
</span>
</li>
{% endfor %}
</ul>
{% else %}
<div class="alert alert-info">
Aucune pièce jointe
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1 @@
{{ mail.textContent|raw }}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>{{ mailing.label }}</title>
{% for item in mails %}
<item>
<title><![CDATA[{{ item.subject|raw }}]]></title>
<link>{{ absolute_url(path('mail_show', {mailing: mailing.id, id: item.id})) }}</link>
<description>
<![CDATA[
{% if item.htmlContent %}
{{ item.htmlContent|raw }}
{% else %}
{{ item.textContent|raw }}
{% endif %}
]]></description>
<guid isPermaLink="false">{{ item.id }}</guid>
<pubDate>{{ item.date|date('r') }}</pubDate>
</item>
{% endfor %}
</channel>
</rss>