Overview
SHA1:fa50a20dd394ce076c3315a0c2279e67a56eb0f4
Date: 2018-07-26 13:07:39
User: bohwaz
Comment:Ajout queue d'envoi d'emails
Timelines: family | ancestors | descendants | both | dev
Downloads: Tarball | ZIP archive
Other Links: files | file ages | folders | manifest
Tags And Properties
Context
2018-07-30
03:15
[a92458a4b5] Possibilité de définir une condition d'affichage (en SQL) du plugin dans le menu, permettant de ne pas l'afficher à tous les membres (user: bohwaz, tags: dev)
2018-07-26
13:07
[fa50a20dd3] Ajout queue d'envoi d'emails (user: bohwaz, tags: dev)
2018-07-24
22:37
[0c01217ecf] Envoi d'email perso en utilisant l'adresse expéditrice forcée ou de l'asso, fix [e7539ae31f] (user: bohwaz, tags: trunk, stable)
2018-07-20
22:15
[1f94d65a20] Erreur plus explicite quand on essaye de modifier une écriture qui n'existe pas (user: bohwaz, tags: dev)
Changes

Modified src/VERSION from [d1cc680d2c] to [37225f3c32].

1
0.8.5
|
1
0.9.0

Modified src/config.dist.php from [2c59c39b5a] to [1f09233dd8].

225
226
227
228
229
230
231
232

233
234
235
236
237
















//const SMTP_PASSWORD = 'abcd';

/**
 * Sécurité du serveur SMTP
 * 
 * NONE = pas de chiffrement
 * SSL = connexion SSL (le plus sécurisé)

 * STARTTLS = utilisation de STARTTLS (moyennement sécurisé)
 *
 * Défaut : STARTTLS
 */
const SMTP_SECURITY = 'STARTTLS';






















|
>





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253

//const SMTP_PASSWORD = 'abcd';

/**
 * Sécurité du serveur SMTP
 * 
 * NONE = pas de chiffrement
 * SSL = connexion SSL native
 * TLS = connexion TLS native (le plus sécurisé)
 * STARTTLS = utilisation de STARTTLS (moyennement sécurisé)
 *
 * Défaut : STARTTLS
 */
const SMTP_SECURITY = 'STARTTLS';

/**
 * Forcer la valeur de l'expéditeur des emails
 * 
 * false, null ou vide = désactivé
 * chaîne = adresse email qui sera utilisé dans le champ From
 * des emails envoyés
 * 
 * Utile pour les services d'envoi SMTP tiers comme Amazon SES.
 * Si activé le "From" sera : "Nom de l'association" <adresse@email.tld>
 * avec le Reply-To positionné sur l'adresse de l'association
 * 
 * Défaut : false
 */
const FORCE_EMAIL_FROM = false;

Modified src/cron.php from [d4a8acbd51] to [12f97b4850].

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


23
24
25

if ($config->get('frequence_sauvegardes') && $config->get('nombre_sauvegardes'))
{
	$s = new Sauvegarde;
	$s->auto();
}


// Exécution des rappels automatiques
$rappels = new Rappels;

if ($rappels->countAll())
{
	$rappels->sendPending();
}



// Nettoyage du cache statique
Static_Cache::clean();







<







>
>



8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26

if ($config->get('frequence_sauvegardes') && $config->get('nombre_sauvegardes'))
{
	$s = new Sauvegarde;
	$s->auto();
}


// Exécution des rappels automatiques
$rappels = new Rappels;

if ($rappels->countAll())
{
	$rappels->sendPending();
}

(new Email)->runQueue();

// Nettoyage du cache statique
Static_Cache::clean();

Modified src/include/data/0.9.0.sql from [8f147a9014] to [2f69fe8735].

6
7
8
9
10
11
12

13
14
15
16
17
18
19
..
20
21
22
23
24
25
26



ALTER TABLE membres_categories RENAME TO membres_categories_old;

-- Mise à jour table compta_rapprochement: la foreign key sur membres est passée
-- à ON DELETE SET NULL
ALTER TABLE compta_rapprochement RENAME TO compta_rapprochement_old;

-- Re-créer la table

.read schema.sql

-- Copie des données, sauf la colonne description
INSERT INTO membres_categories SELECT id, nom, droit_wiki,
	droit_membres, droit_compta, droit_inscription,
	droit_connexion, droit_config, cacher,
	id_cotisation_obligatoire FROM membres_categories_old;
................................................................................

-- Suppression des anciennes tables
DROP TABLE membres_categories_old;

-- Migration des données
INSERT INTO compta_rapprochement SELECT * FROM compta_rapprochement_old;
DROP TABLE compta_rapprochement_old;










>







 







>
>
>
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
..
21
22
23
24
25
26
27
28
29
30
ALTER TABLE membres_categories RENAME TO membres_categories_old;

-- Mise à jour table compta_rapprochement: la foreign key sur membres est passée
-- à ON DELETE SET NULL
ALTER TABLE compta_rapprochement RENAME TO compta_rapprochement_old;

-- Re-créer la table
-- Créer également les nouvelles tables email
.read schema.sql

-- Copie des données, sauf la colonne description
INSERT INTO membres_categories SELECT id, nom, droit_wiki,
	droit_membres, droit_compta, droit_inscription,
	droit_connexion, droit_config, cacher,
	id_cotisation_obligatoire FROM membres_categories_old;
................................................................................

-- Suppression des anciennes tables
DROP TABLE membres_categories_old;

-- Migration des données
INSERT INTO compta_rapprochement SELECT * FROM compta_rapprochement_old;
DROP TABLE compta_rapprochement_old;

-- Cette variable n'est plus utilisée
DELETE FROM config WHERE cle = 'email_envoi_automatique';

Modified src/include/data/schema.sql from [4759146427] to [21009b3120].

381
382
383
384
385
386
387



















CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);


























>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS emails_attente (
-- Emails en attente d'expédition (queue d'envoi)
    id INTEGER NOT NULL PRIMARY KEY,
    adresse TEXT NOT NULL,
    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE,
    sujet TEXT NOT NULL,
    contenu TEXT NOT NULL,
    date_envoi TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_envoi) IS NOT NULL AND datetime(date_envoi) = date_envoi),
    statut INTEGER NOT NULL DEFAULT 0 -- 0 = en attente, 1 = en cours d'envoi
);

CREATE TABLE IF NOT EXISTS emails_rejets (
-- Adresses email qui ne peuvent recevoir de message
    adresse TEXT NOT NULL PRIMARY KEY,
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
    message TEXT NOT NULL,
    statut INTEGER NOT NULL DEFAULT 0 -- -1 = désinscription à l'initiative de l'utilisateur, -2 = boîte mail inexistante, >= 1 = nombre de rejets temporaire
);

Modified src/include/init.php from [a861e81c03] to [5ff1d716c3].

105
106
107
108
109
110
111

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
    'SMTP_HOST'             => false,
    'SMTP_USER'             => null,
    'SMTP_PASSWORD'         => null,
    'SMTP_PORT'             => 587,
    'SMTP_SECURITY'         => 'STARTTLS',
    'ADMIN_URL'             => WWW_URL . 'admin/',
    'NTP_SERVER'            => 'fr.pool.ntp.org',

];

foreach ($default_config as $const => $value)
{
    $const = sprintf('Garradin\\%s', $const);

    if (!defined($const))
    {
        define($const, $value);
    }
}

const WEBSITE = 'http://garradin.eu/';
const PLUGINS_URL = 'https://garradin.eu/plugins/list.json';

// PHP devrait être assez intelligent pour chopper la TZ système mais nan
// il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour 
// éviter le message d'erreur à la con on définit une timezone par défaut
// Pour utiliser une autre timezone, il suffit de définir date.timezone dans
// un .htaccess ou dans config.local.php







>












|







105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
    'SMTP_HOST'             => false,
    'SMTP_USER'             => null,
    'SMTP_PASSWORD'         => null,
    'SMTP_PORT'             => 587,
    'SMTP_SECURITY'         => 'STARTTLS',
    'ADMIN_URL'             => WWW_URL . 'admin/',
    'NTP_SERVER'            => 'fr.pool.ntp.org',
    'FORCE_EMAIL_FROM'      => false,
];

foreach ($default_config as $const => $value)
{
    $const = sprintf('Garradin\\%s', $const);

    if (!defined($const))
    {
        define($const, $value);
    }
}

const WEBSITE = 'https://garradin.eu/';
const PLUGINS_URL = 'https://garradin.eu/plugins/list.json';

// PHP devrait être assez intelligent pour chopper la TZ système mais nan
// il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour 
// éviter le message d'erreur à la con on définit une timezone par défaut
// Pour utiliser une autre timezone, il suffit de définir date.timezone dans
// un .htaccess ou dans config.local.php

Modified src/include/lib/Garradin/Config.php from [a3f009c8e4] to [14cb7536d2].

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
...
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
            'site_asso'               =>  $string,
            
            'monnaie'                 =>  $string,
            'pays'                    =>  $string,
            
            'champs_membres'          =>  $object,
            
            'email_envoi_automatique' => $string,
            
            'categorie_membres'       =>  $int,
            
            'categorie_dons'          =>  $int,
            'categorie_cotisations'   =>  $int,
            
            'accueil_wiki'            =>  $string,
            'accueil_connexion'       =>  $string,
................................................................................
                {
                    $key = str_replace('accueil_', '', $key);
                    throw new UserException('Le nom de la page d\'accueil ' . $key . ' ne peut rester vide.');
                }
                break;
            }
            case 'email_asso':
            case 'email_envoi_automatique':
            {
                if (!filter_var($value, FILTER_VALIDATE_EMAIL))
                {
                    throw new UserException('Adresse e-mail invalide.');
                }
                break;
            }







<
<







 







<







44
45
46
47
48
49
50


51
52
53
54
55
56
57
...
247
248
249
250
251
252
253

254
255
256
257
258
259
260
            'site_asso'               =>  $string,
            
            'monnaie'                 =>  $string,
            'pays'                    =>  $string,
            
            'champs_membres'          =>  $object,
            


            'categorie_membres'       =>  $int,
            
            'categorie_dons'          =>  $int,
            'categorie_cotisations'   =>  $int,
            
            'accueil_wiki'            =>  $string,
            'accueil_connexion'       =>  $string,
................................................................................
                {
                    $key = str_replace('accueil_', '', $key);
                    throw new UserException('Le nom de la page d\'accueil ' . $key . ' ne peut rester vide.');
                }
                break;
            }
            case 'email_asso':

            {
                if (!filter_var($value, FILTER_VALIDATE_EMAIL))
                {
                    throw new UserException('Adresse e-mail invalide.');
                }
                break;
            }

Added src/include/lib/Garradin/Email.php version [5d05a885ef].









































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
<?php

namespace Garradin;

use KD2\Security;

class Email
{
	const STATUT_ATTENTE_ENVOI = 0;
	const STATUT_EN_COURS_ENVOI = 1;

	/**
	 * Valeur de blocage pour les emails qui ont demandé à ne plus recevoir de message
	 */
	const REJET_OPTOUT = -1;

	/**
	 * Valeur de blocage pour les emails qui sont revenus avec une erreur permanente
	 */
	const REJET_DEFINITIF = -2;

	/**
	 * Seuil à partir duquel on n'essaye plus d'envoyer de message à cette adresse
	 */
	const REJET_ABANDON = 3;

	/**
	 * Renvoie la liste des emails en attente d'envoi dans la queue,
	 * sauf ceux qui correspondent à des adresses bloquées
	 * @return array
	 */
	public function listQueue()
	{
		// Nettoyage de la queue déjà
		$this->purgeQueueFromRejected();
		return DB::getInstance()->get('SELECT * FROM emails_attente	WHERE statut = ?;', self::STATUT_ATTENTE_ENVOI);
	}

	/**
	 * Ajoute un message à la queue d'envoi
	 * @param  string $to        Destinataire
	 * @param  string $subject   Sujet du message
	 * @param  string $content   Contenu
	 * @param  integer $id_membre ID membre (facultatif)
	 * @param  string $pgp_key   Clé PGP, si renseigné le message sera chiffré à l'aide de cette clé
	 * @return boolean
	 */
	public function appendToQueue($to, $subject, $content, $id_membre = null, $pgp_key = null)
	{
		// Ne pas envoyer de mail à des adresses invalides
		if (!filter_var($to, FILTER_VALIDATE_EMAIL))
		{
			throw new UserException('Adresse email invalide: ' . $to);
		}

		if ($pgp_key)
		{
			$content = Security::encryptWithPublicKey($pgp_key, $content);
		}

		$content = wordwrap($content);
		$content = trim($content);

		return DB::getInstance()->insert('emails_attente', [
			'adresse'   => $to,
			'id_membre' => (int) $id_membre ?: null,
			'sujet'     => $subject,
			'contenu'   => $content,
		]);
	}

	/**
	 * Lance la queue d'envoi
	 * @return void
	 */
	public function runQueue()
	{
		$res = DB::getInstance()->iterate('SELECT * FROM emails_attente	WHERE statut = ?;', self::STATUT_ATTENTE_ENVOI);

		foreach ($res as $row)
		{
			$this->mail($row->adresse, $row->sujet, $row->message);
		}
	}

	/**
	 * Supprime de la queue les messages liés à des adresses invalides
	 * ou qui ne souhaitent plus recevoir de message
	 * @return boolean
	 */
	public function purgeQueueFromRejected()
	{
		return DB::getInstance()->delete('emails_attente',
			'adresse IN (SELECT adresse FROM emails_rejets WHERE r.statut < 0 OR r.statut > ?)',
			self::REJET_ABANDON);
	}

	/**
	 * Change le statut d'un message dans la queue d'envoi
	 * @param integer $id
	 * @param integer $status
	 * @return boolean
	 */
	public function setMessageStatusInQueue($id, $status)
	{
		if (!in_array($status, [self::STATUT_ATTENTE_ENVOI, self::STATUT_EN_COURS_ENVOI]))
		{
			throw new \UnexpectedValueException('Statut inconnu: ' . $status);
		}

		return DB::getInstance()->update('emails_attente', ['statut' => $status], 'id = ' . (int)$id);
	}

	/**
	 * Supprime un message de la queue d'envoi
	 * @param  integer $id
	 * @return boolean
	 */
	public function deleteFromQueue($id)
	{
		return DB::getInstance()->delete('emails_attente', 'id = ?', (int)$id);
	}

	/**
	 * Tente de trouver le statut de rejet (définitif ou temporaire) d'un message à partir du message d'erreur reçu
	 * @param  string $error_message
	 * @return integer|null
	 */
	public function guessRejectionStatus($error_message)
	{
		if (preg_match('/unavailable|doesn\'t\s*have|quota|does\s*not\s*exist|invalid|Unrouteable|unknown|illegal/i', $error_message))
		{
			return self::REJET_DEFINITIF;
		}
		elseif (preg_match('/rejete|rejected|spam\s*detected|Service\s*refus|greylist/i', $error_message))
		{
			return 1;
		}

		return null;
	}

	/**
	 * Met à jour le statut de rejet d'une adresse
	 * @param string $address
	 * @param integer $status
	 * @param string $message
	 * @return boolean
	 */
	public function setRejectedStatus($address, $status, $message)
	{
		$address = strtolower(trim($address));

		if (!filter_var($address, FILTER_VALIDATE_EMAIL))
		{
			return false;
		}

		if ($status < 0 && !in_array($status, [self::REJET_DEFINITIF, self::REJET_OPTOUT]))
		{
			throw new \UnexpectedValueException('Statut inconnu: ' . $status);
		}

		if ($status == 0)
		{
			throw new \UnexpectedValueException('Statut invalide');
		}

		return DB::getInstance()->preparedQuery('INSERT OR IGNORE INTO emails_rejets (adresse, message, statut) VALUES (?, ?, ?);',
			[$address, $message, $status]);
	}

	/**
	 * Vérifie qu'une adresse est valide
	 * @param  string $address
	 * @return boolean|integer FALSE si l'adresse est invalide (syntaxe) ou un entier si l'adresse a été rejetée
	 */
	static public function checkAddress($address)
	{
		$address = strtolower(trim($address));

		if (!filter_var($address, FILTER_VALIDATE_EMAIL))
		{
			return false;
		}

		// Ce domaine n'existe pas (MX inexistant), erreur de saisie courante
		if (substr($address, -10) == '@gmail.fr')
		{
			return false;
		}

		return DB::getInstance()->firstColumn('SELECT statut FROM emails_rejets WHERE adresse = ?;', $address);
	}

	protected function mail($to, $subject, $content, array $headers = [])
	{
		// Création du contenu du message
		$config = Config::getInstance();

		$subject = sprintf('[%s] %s', $config->get('nom_asso'), $subject);

		$unsubscribe_url = sprintf('%semail.php?optout=%s', ADMIN_URL, rawurlencode($to));

		$content .= sprintf("\n\n-- \n%s\n%s\n\n", $config->get('nom_asso'), $config('site_asso'));
		$content .= "Vous recevez ce message car vous êtes inscrit comme membre de l'association.\n";
		$content .= "Pour ne plus recevoir de message de notre part cliquez ici :\n" . $unsubscribe_url;

		$content = preg_replace("#(?<!\r)\n#si", "\r\n", $content);

		$headers['List-Unsubscribe'] = sprintf('<%s>', $unsubscribe_url);

		if (FORCE_EMAIL_FROM)
		{
			$headers['From'] = sprintf('"%s" <%s>', sprintf('=?UTF-8?B?%s?=', base64_encode($config->get('nom_asso'))), FORCE_EMAIL_FROM);
			$headers['Return-Path'] = FORCE_EMAIL_FROM;
			$headers['Reply-To'] = $config->get('email_asso');
		}
		else
		{
			$headers['From'] = sprintf('"%s" <%s>', sprintf('=?UTF-8?B?%s?=', base64_encode($config->get('nom_asso'))), $config->get('email_asso'));
			$headers['Return-Path'] = $config->get('email_asso');
		}

		$headers['MIME-Version'] = '1.0';
		$headers['Content-type'] = 'text/plain; charset=UTF-8';

		$hash = sha1(uniqid() . var_export([$headers, $to, $subject, $content], true));
		$headers['Message-ID'] = sprintf('%s@%s', $hash, isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname());

		if (SMTP_HOST)
		{
			$const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY);
			
			if (!defined($const))
			{
				throw new \LogicException('Configuration: SMTP_SECURITY n\'a pas une valeur reconnue. Valeurs acceptées: STARTTLS, TLS, SSL, NONE.');
			}

			$secure = constant($const);

			$smtp = new SMTP(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, $secure);
			return $smtp->send($to, $subject, $content, $headers);
		}
		else
		{
			// Encodage du sujet
			$subject = sprintf('=?UTF-8?B?%s?=', base64_encode($subject));
			$raw_headers = '';

			// Sérialisation des entêtes
			foreach ($headers as $name=>$value)
			{
				$raw_headers .= sprintf("%s: %s\r\n", $name, $value);
			}

			return \mail($to, $subject, $content, $raw_headers);
		}
	}
}

Modified src/include/lib/Garradin/Install.php from [6badb0a7e2] to [3900ff2e28].

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
		$config = Config::getInstance();
		$config->set('nom_asso', $nom_asso);
		$config->set('adresse_asso', $adresse_asso);
		$config->set('email_asso', $email_asso);
		$config->set('site_asso', $site_asso);
		$config->set('monnaie', '€');
		$config->set('pays', 'FR');
		$config->set('email_envoi_automatique', $email_asso);
		$config->setVersion(garradin_version());

		$champs = Membres\Champs::importInstall();
		$champs->save(false); // Pas de copie car pas de table membres existante

		$config->set('champ_identifiant', 'email');
		$config->set('champ_identite', 'nom');







<







28
29
30
31
32
33
34

35
36
37
38
39
40
41
		$config = Config::getInstance();
		$config->set('nom_asso', $nom_asso);
		$config->set('adresse_asso', $adresse_asso);
		$config->set('email_asso', $email_asso);
		$config->set('site_asso', $site_asso);
		$config->set('monnaie', '€');
		$config->set('pays', 'FR');

		$config->setVersion(garradin_version());

		$champs = Membres\Champs::importInstall();
		$champs->save(false); // Pas de copie car pas de table membres existante

		$config->set('champ_identifiant', 'email');
		$config->set('champ_identite', 'nom');

Modified src/include/lib/Garradin/Membres.php from [a532ce25c9] to [d2c0429520].

480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
...
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
    /**
     * @deprecated remplacer par envoyer message à tableau de membres
     */
    public function sendMessageToCategory($dest, $sujet, $message, $subscribed_only = false)
    {
        $config = Config::getInstance();

        $headers = [
            'From'  =>  '"'.$config->get('nom_asso').'" <'.$config->get('email_asso').'>',
        ];
        $message .= "\n\n--\n".$config->get('nom_asso')."\n".$config->get('site_asso');

        if ($dest == 0)
            $where = 'id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)';
        else
            $where = 'id_categorie = '.(int)$dest;

        // FIXME: filtrage plus intelligent, car le champ lettre_infos peut ne pas exister
        if ($subscribed_only)
................................................................................
            if ($champs->get('lettre_infos'))
            {
                $where .= ' AND lettre_infos = 1';
            }
        }

        $db = DB::getInstance();
        $res = $db->query('SELECT email FROM membres WHERE LENGTH(email) > 0 AND '.$where.' ORDER BY id;');

        $sujet = '['.$config->get('nom_asso').'] '.$sujet;

        while ($row = $res->fetchArray(SQLITE3_ASSOC))
        {
            Utils::mail($row['email'], $sujet, $message, $headers);
        }

        return true;
    }

    public function searchSQL($query)
    {







<
<
<
<
<







 







|

|

|

|







480
481
482
483
484
485
486





487
488
489
490
491
492
493
...
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
    /**
     * @deprecated remplacer par envoyer message à tableau de membres
     */
    public function sendMessageToCategory($dest, $sujet, $message, $subscribed_only = false)
    {
        $config = Config::getInstance();






        if ($dest == 0)
            $where = 'id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)';
        else
            $where = 'id_categorie = '.(int)$dest;

        // FIXME: filtrage plus intelligent, car le champ lettre_infos peut ne pas exister
        if ($subscribed_only)
................................................................................
            if ($champs->get('lettre_infos'))
            {
                $where .= ' AND lettre_infos = 1';
            }
        }

        $db = DB::getInstance();
        $res = $db->iterate('SELECT email FROM membres WHERE LENGTH(email) > 0 AND '.$where.' ORDER BY id;');

        $email = new Email;

        foreach ($res as $row)
        {
            $email->appendToQueue($row->email, $sujet, $message);
        }

        return true;
    }

    public function searchSQL($query)
    {

Modified src/include/lib/Garradin/Membres/Session.php from [8016f26bf2] to [3934882d0a].

3
4
5
6
7
8
9

10
11
12
13
14
15
16
...
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
...
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
...
284
285
286
287
288
289
290
291
292
293
294
295



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
namespace Garradin\Membres;

use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\Membres;
use Garradin\UserException;


use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;

use KD2\Security;
use KD2\Security_OTP;
................................................................................
		$query = sprintf('%s.%s.%s', $id, $expire, $hash);

		$message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n";
		$message.= "Il vous suffit de cliquer sur le lien ci-dessous pour recevoir un nouveau mot de passe.\n\n";
		$message.= ADMIN_URL . 'password.php?c=' . $query;
		$message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé.";

		Utils::mail($membre->email, '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message, [], $membre->clef_pgp);
		return true;
	}

	static public function recoverPasswordConfirm($code)
	{
		if (substr_count($code, '.') !== 2)
		{
			return false;
................................................................................
		$message.= "Votre nouveau mot de passe : ".$password."\n\n";
		$message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler.";

		$password = Membres::hashPassword($password);

		$db->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$id]);

		return Utils::mail($membre->email, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message, [], $membre->clef_pgp);
	}

	public function editUser($data)
	{
		(new Membres)->edit($this->user->id, $data, false);
		$this->refresh();

................................................................................
		$out['qrcode'] = 'data:image/svg+xml;base64,' . base64_encode($qrcode->toSVG());

		return $out;
	}

	public function sendMessage($dest, $sujet, $message, $copie = false)
	{
		$from = $this->getUser();
		$from = $from->email;
		// Uniquement adresse email pour le moment car faudrait trouver comment
		// indiquer le nom mais qu'il soit correctement échappé FIXME




		$config = Config::getInstance();

		$message .= "\n\n--\nCe message a été envoyé par un membre de ".$config->get('nom_asso');
		$message .= ", merci de contacter ".$config->get('email_asso')." en cas d'abus.";

		if ($copie)
		{
			Utils::mail($from, $sujet, $message);
		}

		return Utils::mail($dest, $sujet, $message, ['From' => $from]);
	}

	public function editSecurity(Array $data = [])
	{
		$allowed_fields = ['passe', 'clef_pgp', 'secret_otp'];

		foreach ($data as $key=>$value)







>







 







|
<







 







|







 







|
<
<
<

>
>
>
|

|
<



|


|







3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
189
190
191
192
193
194
195
196

197
198
199
200
201
202
203
...
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
...
284
285
286
287
288
289
290
291



292
293
294
295
296
297
298

299
300
301
302
303
304
305
306
307
308
309
310
311
312
namespace Garradin\Membres;

use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\Membres;
use Garradin\UserException;
use Garradin\Email;

use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;

use KD2\Security;
use KD2\Security_OTP;
................................................................................
		$query = sprintf('%s.%s.%s', $id, $expire, $hash);

		$message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n";
		$message.= "Il vous suffit de cliquer sur le lien ci-dessous pour recevoir un nouveau mot de passe.\n\n";
		$message.= ADMIN_URL . 'password.php?c=' . $query;
		$message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé.";

		return (new Email)->appendToQueue($membre->email, 'Mot de passe perdu ?', $message, $membre->id, $membre->clef_pgp);

	}

	static public function recoverPasswordConfirm($code)
	{
		if (substr_count($code, '.') !== 2)
		{
			return false;
................................................................................
		$message.= "Votre nouveau mot de passe : ".$password."\n\n";
		$message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler.";

		$password = Membres::hashPassword($password);

		$db->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$id]);

		return (new Email)->appendToQueue($membre->email, 'Nouveau mot de passe', $message, [], $membre->clef_pgp);
	}

	public function editUser($data)
	{
		(new Membres)->edit($this->user->id, $data, false);
		$this->refresh();

................................................................................
		$out['qrcode'] = 'data:image/svg+xml;base64,' . base64_encode($qrcode->toSVG());

		return $out;
	}

	public function sendMessage($dest, $sujet, $message, $copie = false)
	{
		$user = $this->getUser();




		$content = "Ce message vous a été envoyé par :\n";
		$content.= sprintf("%s\n%s\n\n", $user->identite, $user->email);
		$content.= str_repeat('=', 70) . "\n\n";
		$content.= $message;

		$email = new Email;


		if ($copie)
		{
			$email->appendToQueue($user->email, $sujet, $content);
		}

		return $email->appendToQueue($dest, $sujet, $content);
	}

	public function editSecurity(Array $data = [])
	{
		$allowed_fields = ['passe', 'clef_pgp', 'secret_otp'];

		foreach ($data as $key=>$value)

Modified src/include/lib/Garradin/Rappels_Envoyes.php from [4ab2f9f384] to [9c988c2ebc].

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
		$replace['nb_jours'] = abs($replace['nb_jours']);
		$replace['delai'] = abs($replace['delai']);

		$subject = $this->replaceTagsInContent($data->sujet, $replace);
		$text = $this->replaceTagsInContent($data->texte, $replace);

		// Envoi du mail
		Utils::mail($data->email, $subject, $text);

		// Enregistrement en DB
		$this->add([
			'id_cotisation' => $data->id_cotisation,
			'id_membre'     => $data->id,
			'id_rappel'     => $data->id_rappel,
			'media'         => Rappels_Envoyes::MEDIA_EMAIL,







|







146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
		$replace['nb_jours'] = abs($replace['nb_jours']);
		$replace['delai'] = abs($replace['delai']);

		$subject = $this->replaceTagsInContent($data->sujet, $replace);
		$text = $this->replaceTagsInContent($data->texte, $replace);

		// Envoi du mail
		(new Email)->appendToQueue($data->email, $subject, $text);

		// Enregistrement en DB
		$this->add([
			'id_cotisation' => $data->id_cotisation,
			'id_membre'     => $data->id,
			'id_rappel'     => $data->id_rappel,
			'media'         => Rappels_Envoyes::MEDIA_EMAIL,

Modified src/include/lib/Garradin/Utils.php from [7d0bf46225] to [f5f31ae435].

371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
        $str = preg_replace('/<em>(\V*?)<\/em>/', '\'\'$1\'\'', $str);
        $str = preg_replace('/<li>(\V*?)<\/li>/', '* $1', $str);
        $str = preg_replace('/<ul>|<\/ul>/', '', $str);
        $str = preg_replace('/<a href="([^"]*?)">(\V*?)<\/a>/', '[[$2 | $1]]', $str);
        return $str;
    }

    static public function mail($to, $subject, $content, array $headers = [], $pgp_key = null)
    {
        // Création du contenu du message
        $content = wordwrap($content);
        $content = trim($content);

        $content = preg_replace("#(?<!\r)\n#si", "\r\n", $content);
        $config = Config::getInstance();

        if (empty($headers['From']))
        {
            $headers['From'] = sprintf('"%s" <%s>', sprintf('=?UTF-8?B?%s?=', base64_encode($config->get('nom_asso'))), $config->get('email_envoi_automatique'));
        }

        $headers['MIME-Version'] = '1.0';
        $headers['Content-type'] = 'text/plain; charset=UTF-8';
        $headers['Return-Path'] = $config->get('email_envoi_automatique');

        $hash = sha1(uniqid() . var_export([$headers, $to, $subject, $content], true));
        $headers['Message-ID'] = sprintf('%s@%s', $hash, isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname());

        if ($pgp_key)
        {
            $content = Security::encryptWithPublicKey($pgp_key, $content);
        }

        if (!is_array($to))
        {
            $to = [$to];
        }

        foreach ($to as $recipient)
        {
            // Ne pas envoyer de mail à des adresses invalides
            if (!filter_var($recipient, FILTER_VALIDATE_EMAIL))
            {
                continue;
            }

            if (!self::_sendMail($recipient, $subject, $content, $headers))
            {
                throw new \RuntimeException('Impossible d\'envoyer l\'email');
            }
        }

        return true;
    }

    static protected function _sendMail($to, $subject, $content, array $headers)
    {
        if (SMTP_HOST)
        {
            $const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY);
            
            if (!defined($const))
            {
                throw new \LogicException('Configuration: SMTP_SECURITY n\'a pas une valeur reconnue. Valeurs acceptées: STARTTLS, SSL, NONE.');
            }

            $secure = constant($const);

            $smtp = new SMTP(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, $secure);
            return $smtp->send($to, $subject, $content, $headers);
        }
        else
        {
            // Encodage du sujet
            $subject = sprintf('=?UTF-8?B?%s?=', base64_encode($subject));
            $raw_headers = '';

            // Sérialisation des entêtes
            foreach ($headers as $name=>$value)
            {
                $raw_headers .= sprintf("%s: %s\r\n", $name, $value);
            }

            return mail($to, $subject, $content, $raw_headers);
        }
    }

    static public function clearCaches($path = false)
    {
        if (!$path)
        {
            self::clearCaches('compiled');
            self::clearCaches('static');
            return true;







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







371
372
373
374
375
376
377
















































































378
379
380
381
382
383
384
        $str = preg_replace('/<em>(\V*?)<\/em>/', '\'\'$1\'\'', $str);
        $str = preg_replace('/<li>(\V*?)<\/li>/', '* $1', $str);
        $str = preg_replace('/<ul>|<\/ul>/', '', $str);
        $str = preg_replace('/<a href="([^"]*?)">(\V*?)<\/a>/', '[[$2 | $1]]', $str);
        return $str;
    }

















































































    static public function clearCaches($path = false)
    {
        if (!$path)
        {
            self::clearCaches('compiled');
            self::clearCaches('static');
            return true;

Modified src/templates/admin/config/index.tpl from [295b6cf79c] to [8b2246c56b].

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
                <select name="pays" id="f_pays" required="required">
                {foreach from=$pays key="cc" item="nom"}
                    <option value="{$cc}"{if $cc == $config.pays} selected="selected"{/if}>{$nom}</option>
                {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Envois par E-Mail</legend>
        <dl>
            <dt><label for="f_email_envoi_automatique">Adresse E-Mail expéditeur des messages automatiques</label></dt>
            <dd><input type="text" name="email_envoi_automatique" id="f_email_envoi_automatique" value="{form_field data=$config name=email_envoi_automatique}" /></dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Wiki</legend>
        <dl>
            <dt><label for="f_accueil_wiki">Page d'accueil du wiki</label> 
                <b title="(Champ obligatoire)">obligatoire</b></dt>







<
<
<
<
<
<
<
<







51
52
53
54
55
56
57








58
59
60
61
62
63
64
                <select name="pays" id="f_pays" required="required">
                {foreach from=$pays key="cc" item="nom"}
                    <option value="{$cc}"{if $cc == $config.pays} selected="selected"{/if}>{$nom}</option>
                {/foreach}
                </select>
            </dd>
        </dl>








    </fieldset>

    <fieldset>
        <legend>Wiki</legend>
        <dl>
            <dt><label for="f_accueil_wiki">Page d'accueil du wiki</label> 
                <b title="(Champ obligatoire)">obligatoire</b></dt>

Modified src/templates/error.tpl from [f1a21eb4e1] to [52bccc1e68].

1
2
3
4
5
6
7
8
9
10
11
12
..
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Erreur</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style type="text/css">
    {literal}
    * { margin: 0; padding: 0; }

    html { width: 100%; height: 100%; }
    body {
................................................................................
    }
    {/literal}
    </style>
</head>

<body>

<h1>Erreur</h1>

<p class="error">
    {$error|escape|nl2br}
</p>

<p>
    <a href="{$www_url}" onclick="history.back(); return false;">&larr; Retour</a>
</p>

</body>
</html>




|







 







|






|




1
2
3
4
5
6
7
8
9
10
11
12
..
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>{if empty($title)}Erreur{else}{$title}{/if}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style type="text/css">
    {literal}
    * { margin: 0; padding: 0; }

    html { width: 100%; height: 100%; }
    body {
................................................................................
    }
    {/literal}
    </style>
</head>

<body>

<h1>{if empty($title)}Erreur{else}{$title}{/if}</h1>

<p class="error">
    {$error|escape|nl2br}
</p>

<p>
    <a href="{$www_url}" onclick="return history.back();">&larr; Retour</a>
</p>

</body>
</html>

Modified src/www/admin/config/index.php from [cf79456e90] to [402547d004].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (f('save') && $form->check('config'))
{
    try {
        $config->set('nom_asso', f('nom_asso'));
        $config->set('email_asso', f('email_asso'));
        $config->set('adresse_asso', f('adresse_asso'));
        $config->set('site_asso', f('site_asso'));
        $config->set('email_envoi_automatique', f('email_envoi_automatique'));
        $config->set('accueil_wiki', f('accueil_wiki'));
        $config->set('accueil_connexion', f('accueil_connexion'));
        $config->set('categorie_membres', f('categorie_membres'));
        
        $config->set('champ_identite', f('champ_identite'));
        $config->set('champ_identifiant', f('champ_identifiant'));








<







9
10
11
12
13
14
15

16
17
18
19
20
21
22
if (f('save') && $form->check('config'))
{
    try {
        $config->set('nom_asso', f('nom_asso'));
        $config->set('email_asso', f('email_asso'));
        $config->set('adresse_asso', f('adresse_asso'));
        $config->set('site_asso', f('site_asso'));

        $config->set('accueil_wiki', f('accueil_wiki'));
        $config->set('accueil_connexion', f('accueil_connexion'));
        $config->set('categorie_membres', f('categorie_membres'));
        
        $config->set('champ_identite', f('champ_identite'));
        $config->set('champ_identifiant', f('champ_identifiant'));

Added src/www/admin/email.php version [0a473d143d].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace Garradin;

require_once __DIR__ . '/../../include/init.php';

$tpl = Template::getInstance();

if (!empty($_GET['optout']))
{
    $email = new Email;
    $email->setRejectedStatus($_GET['optout'], $email::REJET_OPTOUT, 'Demande de désinscription');
    
    $tpl->assign('title', 'Confirmation');
    $tpl->assign('error', 'Votre adresse a bien été désinscrite, vous ne recevrez plus de messages de notre part.');
}

$tpl->display('error.tpl');