Absenderpruefung bei PayPal-Mails entfernen
This commit is contained in:
+17
-14
@@ -153,14 +153,14 @@ function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?s
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stammt die Mail wirklich von PayPal? Geprueft wird die Domain der
|
* Sieht der Absender nach PayPal aus? Erkennt Landesvarianten (paypal.at,
|
||||||
* Absenderadresse - inklusive Landesvarianten (paypal.at, paypal.co.uk) und
|
* paypal.co.uk) und Versand-Subdomains (e.paypal.de); der Vergleich haengt
|
||||||
* Versand-Subdomains (e.paypal.de), die PayPal tatsaechlich benutzt.
|
* am Ende der Domain, "paypal.de.beispiel.com" ist also kein PayPal.
|
||||||
*
|
*
|
||||||
* Der Vergleich haengt bewusst am Ende der Domain: "paypal.de.beispiel.com"
|
* Nur zur Anzeige in der Diagnose (check-imap-support.php): die
|
||||||
* ist NICHT PayPal, wuerde bei einer simplen Textsuche nach "@paypal.de"
|
* Verarbeitung prueft den Absender bewusst nicht, weil Zahlungsmails auch
|
||||||
* aber durchrutschen - und damit koennte jemand mit Kenntnis der
|
* ueber manuelle Weiterleitungen ankommen. Massgeblich ist die
|
||||||
* Eingangsadresse Zahlungen erfinden.
|
* Empfangsadresse.
|
||||||
*/
|
*/
|
||||||
function paypal_from_is_paypal(string $fromHeader): bool
|
function paypal_from_is_paypal(string $fromHeader): bool
|
||||||
{
|
{
|
||||||
@@ -381,19 +381,22 @@ function paypal_preview(PDO $pdo, int $tenantId, array $parsed): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verarbeitet eine rohe PayPal-Mail vollstaendig: Tenant per Plus-Token,
|
* Verarbeitet eine rohe PayPal-Mail vollstaendig: Mandant per Plus-Token der
|
||||||
* Absenderpruefung, Parsing, Abgleich. Fuer den IMAP-Cron und Tests.
|
* Empfangsadresse, Parsing, Abgleich. Fuer den IMAP-Cron und Tests.
|
||||||
|
*
|
||||||
|
* Der Absender wird nicht ausgewertet - siehe Kommentar im Rumpf.
|
||||||
*
|
*
|
||||||
* @param string $recipient Empfaengeradresse mit Plus-Token
|
* @param string $recipient Empfaengeradresse mit Plus-Token
|
||||||
* @param string $fromHeader From-Header der Mail
|
|
||||||
* @param string $rawBody (dekodierter) Mail-Body
|
* @param string $rawBody (dekodierter) Mail-Body
|
||||||
* @return array{status:string, tenant_id?:int}
|
* @return array{status:string, tenant_id?:int}
|
||||||
*/
|
*/
|
||||||
function paypal_process_raw(PDO $pdo, string $recipient, string $fromHeader, string $rawBody, bool $previewOnly = false): array
|
function paypal_process_raw(PDO $pdo, string $recipient, string $rawBody, bool $previewOnly = false): array
|
||||||
{
|
{
|
||||||
if (!paypal_from_is_paypal($fromHeader)) {
|
// Der Absender wird bewusst nicht geprueft: Zahlungsmails erreichen die
|
||||||
return ['status' => 'not_from_paypal'];
|
// Kaffeeliste auch ueber manuelle Weiterleitungen, und dann steht dort
|
||||||
}
|
// die Adresse des Weiterleitenden statt PayPal. Massgeblich ist allein
|
||||||
|
// die Empfangsadresse - ihr Token bestimmt den Mandanten und ist damit
|
||||||
|
// zugleich das Geheimnis, das den Zugang schuetzt.
|
||||||
$token = paypal_inbox_extract_token($recipient);
|
$token = paypal_inbox_extract_token($recipient);
|
||||||
if ($token === null) {
|
if ($token === null) {
|
||||||
return ['status' => 'no_token'];
|
return ['status' => 'no_token'];
|
||||||
|
|||||||
@@ -250,3 +250,23 @@ Mitglied hinter dieser Adresse fremdes Geld gutgeschrieben. Deshalb:
|
|||||||
- übrig bleibt nur eine Adresse, die **nahe am Namen des Zahlers** steht;
|
- übrig bleibt nur eine Adresse, die **nahe am Namen des Zahlers** steht;
|
||||||
ist keine in Reichweite, wird keine genommen und es bleibt beim
|
ist keine in Reichweite, wird keine genommen und es bleibt beim
|
||||||
Namensvergleich.
|
Namensvergleich.
|
||||||
|
|
||||||
|
### Keine Absenderprüfung
|
||||||
|
|
||||||
|
Stand: 2026-08-21. Die Verarbeitung prüft **nicht**, ob die Mail von PayPal
|
||||||
|
kommt. Der Grund ist praktisch: Zahlungsmails erreichen die Kaffeeliste auch
|
||||||
|
über manuelle Weiterleitungen, und dann steht im Absender die Adresse des
|
||||||
|
Weiterleitenden. Eine Absenderprüfung hätte genau diese Mails abgelehnt
|
||||||
|
(`not_from_paypal`).
|
||||||
|
|
||||||
|
Maßgeblich ist allein die **Empfangsadresse**: ihr Plus-Token bestimmt den
|
||||||
|
Mandanten (`paypal_inbox_extract_token()` → `paypal_inbox_resolve_tenant()`)
|
||||||
|
und ist damit zugleich das Geheimnis, das den Zugang schützt. Wer die
|
||||||
|
Adresse kennt, kann eine Mail im PayPal-Wortlaut schicken und eine
|
||||||
|
Gutschrift auslösen — die Adresse gehört deshalb nicht in Verteiler,
|
||||||
|
Signaturen oder Tickets. Als Bremsen bleiben: der Text muss als
|
||||||
|
PayPal-Zahlungsmail erkennbar sein, jeder Transaktionscode wird nur einmal
|
||||||
|
verbucht, und ohne eindeutige Zuordnung landet alles in der Warteschlange.
|
||||||
|
|
||||||
|
`paypal_from_is_paypal()` gibt es weiterhin, aber nur noch als Anzeige in
|
||||||
|
`scripts/check-imap-support.php` („Absender sieht nach PayPal aus").
|
||||||
|
|||||||
+3
-3
@@ -275,11 +275,11 @@ prüfen:
|
|||||||
|
|
||||||
```
|
```
|
||||||
php scripts/fetch-paypal-payments.php --file=mail.html \
|
php scripts/fetch-paypal-payments.php --file=mail.html \
|
||||||
--recipient=zahlungen+<token>@kaffeeliste.de --from=service@paypal.de [--dry-run]
|
--recipient=zahlungen+<token>@kaffeeliste.de [--dry-run]
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Verarbeitungskette selbst (Absenderprüfung, Token, Parser, Zuordnung,
|
Die Verarbeitungskette selbst (Token, Parser, Zuordnung, Buchung, Dedup,
|
||||||
Buchung, Dedup, Parken) deckt `scripts/check-paypal-inbox-flow.php` als
|
Parken) deckt `scripts/check-paypal-inbox-flow.php` als
|
||||||
Regressionstest ab — alles außer der IMAP-Verbindung.
|
Regressionstest ab — alles außer der IMAP-Verbindung.
|
||||||
|
|
||||||
## Deploy-Ablauf für ein Update
|
## Deploy-Ablauf für ein Update
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ if (PHP_SAPI !== 'cli') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/../app/bootstrap.php';
|
require_once __DIR__ . '/../app/bootstrap.php';
|
||||||
// paypal_from_is_paypal() fuer die Absenderpruefung in der Auflistung.
|
// paypal_from_is_paypal() - in der Auflistung nur als Information; die
|
||||||
|
// Verarbeitung selbst prueft den Absender nicht.
|
||||||
require_once __DIR__ . '/../app/paypal-inbox.php';
|
require_once __DIR__ . '/../app/paypal-inbox.php';
|
||||||
|
|
||||||
function imap_check_line(string $label, string $value): void
|
function imap_check_line(string $label, string $value): void
|
||||||
@@ -192,13 +193,13 @@ if (function_exists('imap_open')) {
|
|||||||
}
|
}
|
||||||
$from = (string) ($overview->from ?? '');
|
$from = (string) ($overview->from ?? '');
|
||||||
printf(
|
printf(
|
||||||
"#%-3d %s Von: %s\n An: %s\n Betreff: %s\n Absender gilt als PayPal: %s\n",
|
"#%-3d %s Von: %s\n An: %s\n Betreff: %s\n Absender sieht nach PayPal aus: %s\n",
|
||||||
$msgno,
|
$msgno,
|
||||||
(int) ($overview->seen ?? 0) === 1 ? 'gelesen ' : 'UNGELESEN',
|
(int) ($overview->seen ?? 0) === 1 ? 'gelesen ' : 'UNGELESEN',
|
||||||
$from,
|
$from,
|
||||||
implode(' | ', $empfaenger) !== '' ? implode(' | ', $empfaenger) : '(keine Empfaengerzeile gefunden)',
|
implode(' | ', $empfaenger) !== '' ? implode(' | ', $empfaenger) : '(keine Empfaengerzeile gefunden)',
|
||||||
(string) ($overview->subject ?? ''),
|
(string) ($overview->subject ?? ''),
|
||||||
paypal_from_is_paypal($from) ? 'ja' : 'NEIN - deshalb not_from_paypal'
|
paypal_from_is_paypal($from) ? 'ja' : 'nein (wird trotzdem verarbeitet)'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
imap_close($mbox);
|
imap_close($mbox);
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Regressionstest fuer die Verarbeitung eingehender PayPal-Mails - alles
|
* Regressionstest fuer die Verarbeitung eingehender PayPal-Mails - alles
|
||||||
* ausser der IMAP-Verbindung selbst: Absenderpruefung, Mandant per
|
* ausser der IMAP-Verbindung selbst: Mandant per Plus-Token der
|
||||||
* Plus-Token, Parser, Zuordnung, Buchung, Dedup, das Parken bei
|
* Empfangsadresse, Parser, Zuordnung (Mailadresse, Name, Mitteilung),
|
||||||
* abgeschalteter PayPal-Funktion und die schreibfreie Vorschau (--dry-run).
|
* Buchung, Dedup, das Parken bei abgeschalteter PayPal-Funktion und die
|
||||||
|
* schreibfreie Vorschau (--dry-run).
|
||||||
*
|
*
|
||||||
* Der IMAP-Teil laesst sich nur gegen ein echtes Postfach pruefen (siehe
|
* Der IMAP-Teil laesst sich nur gegen ein echtes Postfach pruefen (siehe
|
||||||
* docs/deployment.md, Abschnitt PayPal-Postfach).
|
* docs/deployment.md, Abschnitt PayPal-Postfach).
|
||||||
@@ -109,26 +110,26 @@ try {
|
|||||||
$mailA = paypal_flow_mail("Erika Musterfrau {$namensSuffix}", '12,50', $codeA, 'Kaffeekasse', '12,15');
|
$mailA = paypal_flow_mail("Erika Musterfrau {$namensSuffix}", '12,50', $codeA, 'Kaffeekasse', '12,15');
|
||||||
|
|
||||||
// Erst die Vorschau: sie darf nichts anlegen.
|
// Erst die Vorschau: sie darf nichts anlegen.
|
||||||
$vorschau = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailA, true);
|
$vorschau = paypal_process_raw($pdo, $empfaengerA, $mailA, true);
|
||||||
paypal_flow_assert('Vorschau meldet die geplante Buchung', ($vorschau['status'] ?? '') === 'would_book', $failures, $passes);
|
paypal_flow_assert('Vorschau meldet die geplante Buchung', ($vorschau['status'] ?? '') === 'would_book', $failures, $passes);
|
||||||
paypal_flow_assert('Vorschau nennt das Mitglied', str_contains((string) ($vorschau['participant'] ?? ''), 'Erika'), $failures, $passes);
|
paypal_flow_assert('Vorschau nennt das Mitglied', str_contains((string) ($vorschau['participant'] ?? ''), 'Erika'), $failures, $passes);
|
||||||
$zahlungenNachVorschau = (int) $pdo->query('SELECT COUNT(*) FROM paypal_payments')->fetchColumn();
|
$zahlungenNachVorschau = (int) $pdo->query('SELECT COUNT(*) FROM paypal_payments')->fetchColumn();
|
||||||
|
|
||||||
$ergebnisA = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailA);
|
$ergebnisA = paypal_process_raw($pdo, $empfaengerA, $mailA);
|
||||||
paypal_flow_assert('eindeutiger Name wird automatisch gebucht', ($ergebnisA['status'] ?? '') === 'booked', $failures, $passes);
|
paypal_flow_assert('eindeutiger Name wird automatisch gebucht', ($ergebnisA['status'] ?? '') === 'booked', $failures, $passes);
|
||||||
paypal_flow_assert('Vorschau hat nichts gespeichert', $zahlungenNachVorschau + 1 === (int) $pdo->query('SELECT COUNT(*) FROM paypal_payments')->fetchColumn(), $failures, $passes);
|
paypal_flow_assert('Vorschau hat nichts gespeichert', $zahlungenNachVorschau + 1 === (int) $pdo->query('SELECT COUNT(*) FROM paypal_payments')->fetchColumn(), $failures, $passes);
|
||||||
// Gebucht wird die "Summe" (nach Gebuehr), nicht der Sendebetrag.
|
// Gebucht wird die "Summe" (nach Gebuehr), nicht der Sendebetrag.
|
||||||
paypal_flow_assert('gebucht wird der Netto-Betrag (12,15 EUR)', paypal_flow_balance($pdo, $participantA) === 1215, $failures, $passes);
|
paypal_flow_assert('gebucht wird der Netto-Betrag (12,15 EUR)', paypal_flow_balance($pdo, $participantA) === 1215, $failures, $passes);
|
||||||
|
|
||||||
// --- Dedup: dieselbe Mail ein zweites Mal
|
// --- Dedup: dieselbe Mail ein zweites Mal
|
||||||
$ergebnisDoppelt = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailA);
|
$ergebnisDoppelt = paypal_process_raw($pdo, $empfaengerA, $mailA);
|
||||||
paypal_flow_assert('dieselbe Transaktion wird nicht doppelt gebucht', ($ergebnisDoppelt['status'] ?? '') === 'duplicate', $failures, $passes);
|
paypal_flow_assert('dieselbe Transaktion wird nicht doppelt gebucht', ($ergebnisDoppelt['status'] ?? '') === 'duplicate', $failures, $passes);
|
||||||
paypal_flow_assert('Guthaben bleibt nach der Dublette unveraendert', paypal_flow_balance($pdo, $participantA) === 1215, $failures, $passes);
|
paypal_flow_assert('Guthaben bleibt nach der Dublette unveraendert', paypal_flow_balance($pdo, $participantA) === 1215, $failures, $passes);
|
||||||
|
|
||||||
// --- Fall 2: unbekannter Zahler -> Warteschlange
|
// --- Fall 2: unbekannter Zahler -> Warteschlange
|
||||||
$codeB = 'TXNQUEUE' . strtoupper(bin2hex(random_bytes(4)));
|
$codeB = 'TXNQUEUE' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$mailB = paypal_flow_mail('Unbekannter Zahler', '5,00', $codeB, 'ohne Zuordnung');
|
$mailB = paypal_flow_mail('Unbekannter Zahler', '5,00', $codeB, 'ohne Zuordnung');
|
||||||
$ergebnisB = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailB);
|
$ergebnisB = paypal_process_raw($pdo, $empfaengerA, $mailB);
|
||||||
paypal_flow_assert('unbekannter Zahler landet in der Warteschlange', ($ergebnisB['status'] ?? '') === 'unmatched', $failures, $passes);
|
paypal_flow_assert('unbekannter Zahler landet in der Warteschlange', ($ergebnisB['status'] ?? '') === 'unmatched', $failures, $passes);
|
||||||
paypal_flow_assert('Warteschlange enthaelt die Zahlung', paypal_count_unmatched($pdo, $tenantA) === 1, $failures, $passes);
|
paypal_flow_assert('Warteschlange enthaelt die Zahlung', paypal_count_unmatched($pdo, $tenantA) === 1, $failures, $passes);
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ try {
|
|||||||
$perMailAdresse = strtolower("karin-kassierer-{$namensSuffix}") . '-' . $suffix . 'm@test.local';
|
$perMailAdresse = strtolower("karin-kassierer-{$namensSuffix}") . '-' . $suffix . 'm@test.local';
|
||||||
$codeM = 'TXNMAIL' . strtoupper(bin2hex(random_bytes(4)));
|
$codeM = 'TXNMAIL' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$mailM = paypal_flow_mail('Voellig Anderer Name', '6,00', $codeM, 'per Adresse', null, $perMailAdresse);
|
$mailM = paypal_flow_mail('Voellig Anderer Name', '6,00', $codeM, 'per Adresse', null, $perMailAdresse);
|
||||||
$ergebnisM = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailM);
|
$ergebnisM = paypal_process_raw($pdo, $empfaengerA, $mailM);
|
||||||
paypal_flow_assert('Zahlung wird ueber die Mailadresse zugeordnet', ($ergebnisM['status'] ?? '') === 'booked', $failures, $passes);
|
paypal_flow_assert('Zahlung wird ueber die Mailadresse zugeordnet', ($ergebnisM['status'] ?? '') === 'booked', $failures, $passes);
|
||||||
paypal_flow_assert('Betrag landet beim Mitglied mit dieser Adresse', paypal_flow_balance($pdo, $perMailId) === 600, $failures, $passes);
|
paypal_flow_assert('Betrag landet beim Mitglied mit dieser Adresse', paypal_flow_balance($pdo, $perMailId) === 600, $failures, $passes);
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ try {
|
|||||||
// Mail wird verworfen.
|
// Mail wird verworfen.
|
||||||
$codeSelf = 'TXNSELF' . strtoupper(bin2hex(random_bytes(4)));
|
$codeSelf = 'TXNSELF' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$mailSelf = paypal_flow_mail('Noch Ein Zahler', '1,00', $codeSelf, 'eigene Adresse', null, $empfaengerA);
|
$mailSelf = paypal_flow_mail('Noch Ein Zahler', '1,00', $codeSelf, 'eigene Adresse', null, $empfaengerA);
|
||||||
$ergebnisSelf = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailSelf);
|
$ergebnisSelf = paypal_process_raw($pdo, $empfaengerA, $mailSelf);
|
||||||
paypal_flow_assert('eigene Empfaengeradresse wird nicht zur Zahleradresse', ($ergebnisSelf['status'] ?? '') === 'unmatched', $failures, $passes);
|
paypal_flow_assert('eigene Empfaengeradresse wird nicht zur Zahleradresse', ($ergebnisSelf['status'] ?? '') === 'unmatched', $failures, $passes);
|
||||||
$selfAdresse = $pdo->prepare('SELECT payer_email FROM paypal_payments WHERE transaction_code = ?');
|
$selfAdresse = $pdo->prepare('SELECT payer_email FROM paypal_payments WHERE transaction_code = ?');
|
||||||
$selfAdresse->execute([$codeSelf]);
|
$selfAdresse->execute([$codeSelf]);
|
||||||
@@ -178,7 +179,7 @@ try {
|
|||||||
$codeE = 'TXNLERN' . strtoupper(bin2hex(random_bytes(4)));
|
$codeE = 'TXNLERN' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$zahlername = "Bernd B. Beispiel-Handel {$namensSuffix}";
|
$zahlername = "Bernd B. Beispiel-Handel {$namensSuffix}";
|
||||||
$mailE = paypal_flow_mail($zahlername, '4,00', $codeE, 'erste Zahlung');
|
$mailE = paypal_flow_mail($zahlername, '4,00', $codeE, 'erste Zahlung');
|
||||||
$ergebnisE = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailE);
|
$ergebnisE = paypal_process_raw($pdo, $empfaengerA, $mailE);
|
||||||
paypal_flow_assert('abweichender Zahlername landet zunaechst in der Warteschlange', ($ergebnisE['status'] ?? '') === 'unmatched', $failures, $passes);
|
paypal_flow_assert('abweichender Zahlername landet zunaechst in der Warteschlange', ($ergebnisE['status'] ?? '') === 'unmatched', $failures, $passes);
|
||||||
|
|
||||||
$zuordnung = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisE['payment_id'], $abweichend, null);
|
$zuordnung = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisE['payment_id'], $abweichend, null);
|
||||||
@@ -187,14 +188,14 @@ try {
|
|||||||
|
|
||||||
$codeF = 'TXNAUTO2' . strtoupper(bin2hex(random_bytes(4)));
|
$codeF = 'TXNAUTO2' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$mailF = paypal_flow_mail($zahlername, '4,00', $codeF, 'zweite Zahlung');
|
$mailF = paypal_flow_mail($zahlername, '4,00', $codeF, 'zweite Zahlung');
|
||||||
$ergebnisF = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailF);
|
$ergebnisF = paypal_process_raw($pdo, $empfaengerA, $mailF);
|
||||||
paypal_flow_assert('zweite Zahlung desselben Zahlers wird automatisch gebucht', ($ergebnisF['status'] ?? '') === 'booked', $failures, $passes);
|
paypal_flow_assert('zweite Zahlung desselben Zahlers wird automatisch gebucht', ($ergebnisF['status'] ?? '') === 'booked', $failures, $passes);
|
||||||
paypal_flow_assert('beide Zahlungen sind gutgeschrieben', paypal_flow_balance($pdo, $abweichend) === 800, $failures, $passes);
|
paypal_flow_assert('beide Zahlungen sind gutgeschrieben', paypal_flow_balance($pdo, $abweichend) === 800, $failures, $passes);
|
||||||
|
|
||||||
// Ein bereits gepflegter PayPal-Name wird nicht ueberschrieben.
|
// Ein bereits gepflegter PayPal-Name wird nicht ueberschrieben.
|
||||||
$codeG = 'TXNKEEP' . strtoupper(bin2hex(random_bytes(4)));
|
$codeG = 'TXNKEEP' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$mailG = paypal_flow_mail('Ganz Anderer Name', '2,00', $codeG, 'fremder Name');
|
$mailG = paypal_flow_mail('Ganz Anderer Name', '2,00', $codeG, 'fremder Name');
|
||||||
$ergebnisG = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailG);
|
$ergebnisG = paypal_process_raw($pdo, $empfaengerA, $mailG);
|
||||||
$zuordnungG = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisG['payment_id'], $abweichend, null);
|
$zuordnungG = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisG['payment_id'], $abweichend, null);
|
||||||
paypal_flow_assert('gepflegter PayPal-Name bleibt unveraendert', ($zuordnungG['learned_paypal_name'] ?? null) === null, $failures, $passes);
|
paypal_flow_assert('gepflegter PayPal-Name bleibt unveraendert', ($zuordnungG['learned_paypal_name'] ?? null) === null, $failures, $passes);
|
||||||
$gespeichert = $pdo->prepare('SELECT paypal_name FROM participants WHERE id = ?');
|
$gespeichert = $pdo->prepare('SELECT paypal_name FROM participants WHERE id = ?');
|
||||||
@@ -210,10 +211,10 @@ try {
|
|||||||
|
|
||||||
$codeC = 'TXNPARK' . strtoupper(bin2hex(random_bytes(4)));
|
$codeC = 'TXNPARK' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$mailC = paypal_flow_mail("Max Mustermann {$namensSuffix}", '7,00', $codeC, 'trotzdem gezahlt');
|
$mailC = paypal_flow_mail("Max Mustermann {$namensSuffix}", '7,00', $codeC, 'trotzdem gezahlt');
|
||||||
$vorschauC = paypal_process_raw($pdo, $empfaengerB, 'service@paypal.de', $mailC, true);
|
$vorschauC = paypal_process_raw($pdo, $empfaengerB, $mailC, true);
|
||||||
paypal_flow_assert('Vorschau meldet das Parken', ($vorschauC['status'] ?? '') === 'would_park', $failures, $passes);
|
paypal_flow_assert('Vorschau meldet das Parken', ($vorschauC['status'] ?? '') === 'would_park', $failures, $passes);
|
||||||
|
|
||||||
$ergebnisC = paypal_process_raw($pdo, $empfaengerB, 'service@paypal.de', $mailC);
|
$ergebnisC = paypal_process_raw($pdo, $empfaengerB, $mailC);
|
||||||
paypal_flow_assert('abgeschaltetes PayPal parkt die Zahlung', ($ergebnisC['status'] ?? '') === 'parked', $failures, $passes);
|
paypal_flow_assert('abgeschaltetes PayPal parkt die Zahlung', ($ergebnisC['status'] ?? '') === 'parked', $failures, $passes);
|
||||||
paypal_flow_assert('geparkte Zahlung wird nicht gebucht', paypal_flow_balance($pdo, $participantB) === 0, $failures, $passes);
|
paypal_flow_assert('geparkte Zahlung wird nicht gebucht', paypal_flow_balance($pdo, $participantB) === 0, $failures, $passes);
|
||||||
paypal_flow_assert('geparkte Zahlung steht in der Warteschlange', paypal_count_unmatched($pdo, $tenantB) === 1, $failures, $passes);
|
paypal_flow_assert('geparkte Zahlung steht in der Warteschlange', paypal_count_unmatched($pdo, $tenantB) === 1, $failures, $passes);
|
||||||
@@ -227,15 +228,18 @@ try {
|
|||||||
|
|
||||||
$codeD = 'TXNLOCK' . strtoupper(bin2hex(random_bytes(4)));
|
$codeD = 'TXNLOCK' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
$mailD = paypal_flow_mail("Lisa Beispiel {$namensSuffix}", '3,00', $codeD, 'gesperrt');
|
$mailD = paypal_flow_mail("Lisa Beispiel {$namensSuffix}", '3,00', $codeD, 'gesperrt');
|
||||||
$ergebnisD = paypal_process_raw($pdo, "zahlungen+{$tokenC}@kaffeeliste.de", 'service@paypal.de', $mailD);
|
$ergebnisD = paypal_process_raw($pdo, "zahlungen+{$tokenC}@kaffeeliste.de", $mailD);
|
||||||
paypal_flow_assert('Betreiber-Sperre parkt die Zahlung ebenfalls', ($ergebnisD['status'] ?? '') === 'parked', $failures, $passes);
|
paypal_flow_assert('Betreiber-Sperre parkt die Zahlung ebenfalls', ($ergebnisD['status'] ?? '') === 'parked', $failures, $passes);
|
||||||
|
|
||||||
// --- Fall 5: alles, was gar nicht erst verarbeitet werden darf
|
// --- Fall 5: alles, was gar nicht erst verarbeitet werden darf
|
||||||
$fremd = paypal_process_raw($pdo, $empfaengerA, 'no-reply@beispiel.de', $mailA);
|
// Der Absender wird bewusst nicht geprueft: eine von Hand weitergeleitete
|
||||||
paypal_flow_assert('Mail von fremdem Absender wird abgelehnt', ($fremd['status'] ?? '') === 'not_from_paypal', $failures, $passes);
|
// Mail traegt die Adresse des Weiterleitenden, soll aber ankommen.
|
||||||
|
$codeW = 'TXNFWD' . strtoupper(bin2hex(random_bytes(4)));
|
||||||
|
$mailW = paypal_flow_mail("Erika Musterfrau {$namensSuffix}", '3,50', $codeW, 'weitergeleitet');
|
||||||
|
$weitergeleitet = paypal_process_raw($pdo, $empfaengerA, $mailW);
|
||||||
|
paypal_flow_assert('weitergeleitete Mail wird unabhaengig vom Absender verarbeitet', ($weitergeleitet['status'] ?? '') === 'booked', $failures, $passes);
|
||||||
|
|
||||||
// Absenderpruefung: PayPal verschickt aus mehreren Domains, eine
|
// Die Absendererkennung dient nur noch der Diagnose-Anzeige.
|
||||||
// Lookalike-Domain darf aber nie durchgehen.
|
|
||||||
foreach ([
|
foreach ([
|
||||||
'"PayPal" <service@paypal.de>' => true,
|
'"PayPal" <service@paypal.de>' => true,
|
||||||
'service@paypal.com' => true,
|
'service@paypal.com' => true,
|
||||||
@@ -255,13 +259,13 @@ try {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ohneToken = paypal_process_raw($pdo, 'zahlungen@kaffeeliste.de', 'service@paypal.de', $mailA);
|
$ohneToken = paypal_process_raw($pdo, 'zahlungen@kaffeeliste.de', $mailA);
|
||||||
paypal_flow_assert('Adresse ohne Token wird abgelehnt', ($ohneToken['status'] ?? '') === 'no_token', $failures, $passes);
|
paypal_flow_assert('Adresse ohne Token wird abgelehnt', ($ohneToken['status'] ?? '') === 'no_token', $failures, $passes);
|
||||||
|
|
||||||
$falscherToken = paypal_process_raw($pdo, 'zahlungen+gibtesnicht@kaffeeliste.de', 'service@paypal.de', $mailA);
|
$falscherToken = paypal_process_raw($pdo, 'zahlungen+gibtesnicht@kaffeeliste.de', $mailA);
|
||||||
paypal_flow_assert('unbekannter Token wird abgelehnt', ($falscherToken['status'] ?? '') === 'unknown_tenant', $failures, $passes);
|
paypal_flow_assert('unbekannter Token wird abgelehnt', ($falscherToken['status'] ?? '') === 'unknown_tenant', $failures, $passes);
|
||||||
|
|
||||||
$keineZahlung = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', '<html><body>Newsletter von PayPal</body></html>');
|
$keineZahlung = paypal_process_raw($pdo, $empfaengerA, '<html><body>Newsletter von PayPal</body></html>');
|
||||||
paypal_flow_assert('Werbemail wird nicht als Zahlung erkannt', ($keineZahlung['status'] ?? '') === 'not_a_payment', $failures, $passes);
|
paypal_flow_assert('Werbemail wird nicht als Zahlung erkannt', ($keineZahlung['status'] ?? '') === 'not_a_payment', $failures, $passes);
|
||||||
} finally {
|
} finally {
|
||||||
foreach ($tenantIds as $tenantId) {
|
foreach ($tenantIds as $tenantId) {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ declare(strict_types=1);
|
|||||||
*
|
*
|
||||||
* Ablauf pro ungelesener Mail:
|
* Ablauf pro ungelesener Mail:
|
||||||
* 1. Mandant ueber den Plus-Token der Empfaengeradresse bestimmen
|
* 1. Mandant ueber den Plus-Token der Empfaengeradresse bestimmen
|
||||||
* (zahlungen+<token>@…).
|
* (zahlungen+<token>@…). Der Absender wird bewusst nicht geprueft -
|
||||||
* 2. Absender gegen paypal.de/.com pruefen.
|
* Zahlungsmails kommen auch ueber manuelle Weiterleitungen an.
|
||||||
* 3. Body parsen, Netto ermitteln, per Transaktionscode deduplizieren.
|
* 2. Body parsen, Netto ermitteln, per Transaktionscode deduplizieren.
|
||||||
* 4. Eindeutiger Namens-Match -> automatisch als Einzahlung buchen,
|
* 3. Eindeutiger Namens-Match -> automatisch als Einzahlung buchen,
|
||||||
* sonst in die Warteschlange legen. Ist PayPal fuer den Mandanten
|
* sonst in die Warteschlange legen. Ist PayPal fuer den Mandanten
|
||||||
* abgeschaltet, wird die Zahlung nur geparkt (gespeichert, nicht
|
* abgeschaltet, wird die Zahlung nur geparkt (gespeichert, nicht
|
||||||
* gebucht).
|
* gebucht).
|
||||||
@@ -31,7 +31,7 @@ declare(strict_types=1);
|
|||||||
*
|
*
|
||||||
* Testmodus ohne IMAP (eine Rohmail aus einer Datei verarbeiten):
|
* Testmodus ohne IMAP (eine Rohmail aus einer Datei verarbeiten):
|
||||||
* php scripts/fetch-paypal-payments.php --file=mail.html \
|
* php scripts/fetch-paypal-payments.php --file=mail.html \
|
||||||
* --recipient=zahlungen+abc123@kaffeeliste.de --from=service@paypal.de
|
* --recipient=zahlungen+abc123@kaffeeliste.de
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (PHP_SAPI !== 'cli') {
|
if (PHP_SAPI !== 'cli') {
|
||||||
@@ -42,7 +42,7 @@ if (PHP_SAPI !== 'cli') {
|
|||||||
require_once __DIR__ . '/../app/paypal-inbox.php';
|
require_once __DIR__ . '/../app/paypal-inbox.php';
|
||||||
|
|
||||||
$pdo = app_db_pdo();
|
$pdo = app_db_pdo();
|
||||||
$opts = getopt('', ['file:', 'recipient:', 'from:', 'dry-run']);
|
$opts = getopt('', ['file:', 'recipient:', 'dry-run']);
|
||||||
|
|
||||||
// ---- Testmodus: eine Datei verarbeiten (ohne IMAP) --------------------------
|
// ---- Testmodus: eine Datei verarbeiten (ohne IMAP) --------------------------
|
||||||
if (isset($opts['file'])) {
|
if (isset($opts['file'])) {
|
||||||
@@ -52,8 +52,7 @@ if (isset($opts['file'])) {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
$recipient = (string) ($opts['recipient'] ?? '');
|
$recipient = (string) ($opts['recipient'] ?? '');
|
||||||
$from = (string) ($opts['from'] ?? 'service@paypal.de');
|
$result = paypal_process_raw($pdo, $recipient, $body, isset($opts['dry-run']));
|
||||||
$result = paypal_process_raw($pdo, $recipient, $from, $body, isset($opts['dry-run']));
|
|
||||||
echo 'Ergebnis: ' . json_encode($result, JSON_UNESCAPED_UNICODE) . "\n";
|
echo 'Ergebnis: ' . json_encode($result, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
@@ -151,12 +150,11 @@ for ($msgno = 1; $msgno <= $messageCount; $msgno++) {
|
|||||||
if (preg_match_all('/^(?:Delivered-To|X-Original-To|To|X-Forwarded-To):\s*(.+)$/im', $rawHeader, $all)) {
|
if (preg_match_all('/^(?:Delivered-To|X-Original-To|To|X-Forwarded-To):\s*(.+)$/im', $rawHeader, $all)) {
|
||||||
$recipient = implode(' ', $all[1]);
|
$recipient = implode(' ', $all[1]);
|
||||||
}
|
}
|
||||||
$from = (string) ($overview->from ?? '');
|
|
||||||
|
|
||||||
$body = paypal_imap_fetch_body($mbox, $msgno);
|
$body = paypal_imap_fetch_body($mbox, $msgno);
|
||||||
// Im Probelauf wird nichts gespeichert und nichts gebucht - die Vorschau
|
// Im Probelauf wird nichts gespeichert und nichts gebucht - die Vorschau
|
||||||
// sagt nur, was passieren wuerde.
|
// sagt nur, was passieren wuerde.
|
||||||
$result = paypal_process_raw($pdo, $recipient, $from, $body, $dryRun);
|
$result = paypal_process_raw($pdo, $recipient, $body, $dryRun);
|
||||||
|
|
||||||
switch ($result['status']) {
|
switch ($result['status']) {
|
||||||
case 'booked':
|
case 'booked':
|
||||||
|
|||||||
Reference in New Issue
Block a user