") oder ein Array * solcher Listen. Ungueltiges faellt weg, doppelte Adressen werden * zusammengefasst. * * @param mixed $value Text oder Array. * @return array */ public static function parse_addresses( $value ) { $parts = array(); foreach ( is_array( $value ) ? $value : array( $value ) as $item ) { if ( is_string( $item ) ) { $parts = array_merge( $parts, self::split_list( $item ) ); } } $out = array(); $seen = array(); foreach ( $parts as $part ) { $address = self::parse_one( $part ); if ( null === $address ) { continue; } $key = strtolower( $address['email'] ); if ( isset( $seen[ $key ] ) ) { continue; } $seen[ $key ] = true; $out[] = $address; if ( count( $out ) >= self::MAX_RECIPIENTS ) { break; } } return $out; } /** * Zerlegt die Kopfzeilen einer Mail. * * @param mixed $headers Text (Zeilen) oder Array von Zeilen. * @return array{from: array{name: string, email: string}|null, cc: array, bcc: array, reply_to: array, content_type: string, lines: string[]} */ public static function parse_headers( $headers ) { if ( is_string( $headers ) ) { $lines = preg_split( '/\r\n|\r|\n/', $headers ); } elseif ( is_array( $headers ) ) { $lines = array(); foreach ( $headers as $line ) { if ( is_string( $line ) ) { $lines = array_merge( $lines, preg_split( '/\r\n|\r|\n/', $line ) ); } } } else { $lines = array(); } $result = array( 'from' => null, 'cc' => array(), 'bcc' => array(), 'reply_to' => array(), 'content_type' => '', 'lines' => array(), ); foreach ( $lines as $line ) { $line = trim( self::strip_control( (string) $line ) ); if ( '' === $line || false === strpos( $line, ':' ) ) { continue; } list( $name, $value ) = array_map( 'trim', explode( ':', $line, 2 ) ); $lower = strtolower( $name ); switch ( $lower ) { case 'cc': $result['cc'] = array_merge( $result['cc'], self::parse_addresses( $value ) ); continue 2; // Empfaenger-Kopfzeilen werden nicht als Rohzeile gespeichert. case 'bcc': $result['bcc'] = array_merge( $result['bcc'], self::parse_addresses( $value ) ); continue 2; case 'from': $from = self::parse_addresses( $value ); if ( $from && null === $result['from'] ) { $result['from'] = $from[0]; } break; case 'reply-to': $result['reply_to'] = array_merge( $result['reply_to'], self::parse_addresses( $value ) ); break; case 'content-type': $result['content_type'] = strtolower( $value ); break; } if ( count( $result['lines'] ) < self::MAX_HEADER_LINES ) { $result['lines'][] = mb_substr( $name . ': ' . $value, 0, self::MAX_HEADER_LINE ); } } return $result; } /** * Bereitet eine Mail fuer die Speicherung auf. * * @param array $atts Argumente von wp_mail() (to, subject, message, headers, attachments). * @param int $max_body Hoechstlaenge des Textes in Byte. * @return array{subject: string, recipients: array, sender: string, headers: string, body: string, truncated: bool, is_html: bool, attachments: string[]} */ public static function normalize( array $atts, $max_body = self::MAX_BODY ) { $headers = self::parse_headers( $atts['headers'] ?? '' ); $recipients = array(); $seen = array(); $groups = array( 'to' => self::parse_addresses( $atts['to'] ?? '' ), 'cc' => $headers['cc'], 'bcc' => $headers['bcc'], ); foreach ( $groups as $type => $addresses ) { foreach ( $addresses as $address ) { $key = $type . '|' . strtolower( $address['email'] ); if ( isset( $seen[ $key ] ) || count( $recipients ) >= self::MAX_RECIPIENTS ) { continue; } $seen[ $key ] = true; $recipients[] = array( 'type' => $type, 'name' => $address['name'], 'email' => $address['email'], ); } } $subject = trim( preg_replace( '/\s+/', ' ', self::strip_control( (string) ( is_scalar( $atts['subject'] ?? '' ) ? $atts['subject'] : '' ) ) ) ); $subject = mb_substr( $subject, 0, self::MAX_SUBJECT ); $body = is_scalar( $atts['message'] ?? '' ) ? (string) $atts['message'] : ''; $truncated = false; $max_body = max( 1, (int) $max_body ); if ( strlen( $body ) > $max_body ) { $body = mb_strcut( $body, 0, $max_body, 'UTF-8' ); $truncated = true; } $sender = ''; if ( is_array( $headers['from'] ) ) { $sender = '' !== $headers['from']['name'] ? $headers['from']['name'] . ' <' . $headers['from']['email'] . '>' : $headers['from']['email']; } return array( 'subject' => $subject, 'recipients' => $recipients, 'sender' => mb_substr( $sender, 0, 255 ), 'headers' => implode( "\n", $headers['lines'] ), 'body' => $body, 'truncated' => $truncated, 'is_html' => false !== strpos( $headers['content_type'], 'text/html' ), 'attachments' => self::attachment_names( $atts['attachments'] ?? array() ), ); } /** * Nur die Dateinamen der Anhaenge (WordPress erlaubt ein Array oder Pfade * je Zeile). Der Inhalt wird nie gelesen. * * @param mixed $attachments Anhaenge. * @return string[] */ public static function attachment_names( $attachments ) { if ( is_string( $attachments ) ) { $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) ); } if ( ! is_array( $attachments ) ) { return array(); } $names = array(); foreach ( $attachments as $path ) { if ( ! is_string( $path ) || '' === trim( $path ) ) { continue; } $name = basename( str_replace( '\\', '/', trim( $path ) ) ); $name = trim( self::strip_control( $name ) ); if ( '' !== $name ) { $names[] = mb_substr( $name, 0, 255 ); } if ( count( $names ) >= self::MAX_ATTACHMENTS ) { break; } } return $names; } /** * Teilt eine Adressliste an Kommas, die nicht in Anfuehrungszeichen oder * spitzen Klammern stehen. * * @param string $list Liste. * @return string[] */ private static function split_list( $list ) { $parts = array(); $current = ''; $quoted = false; $angle = false; $length = strlen( $list ); for ( $i = 0; $i < $length; $i++ ) { $char = $list[ $i ]; if ( '"' === $char ) { $quoted = ! $quoted; } elseif ( '<' === $char && ! $quoted ) { $angle = true; } elseif ( '>' === $char && ! $quoted ) { $angle = false; } if ( ( ',' === $char || ';' === $char ) && ! $quoted && ! $angle ) { $parts[] = $current; $current = ''; continue; } $current .= $char; } $parts[] = $current; return array_values( array_filter( array_map( 'trim', $parts ), 'strlen' ) ); } /** * @param string $part Eine Adresse ("Name " oder "a@x.de"). * @return array{name: string, email: string}|null */ private static function parse_one( $part ) { $part = trim( self::strip_control( $part ) ); $name = ''; $email = $part; if ( preg_match( '/^(.*)<([^<>]+)>\s*$/s', $part, $m ) ) { $name = trim( trim( $m[1] ), " \t\"'" ); $email = trim( $m[2] ); } if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) { return null; } return array( 'name' => mb_substr( $name, 0, 120 ), 'email' => mb_substr( $email, 0, 254 ), ); } /** * @param string $text Text. * @return string Ohne Steuerzeichen (auch Zeilenumbrueche). */ private static function strip_control( $text ) { return (string) preg_replace( '/[\x00-\x1F\x7F]/', ' ', $text ); } }