zbateson /
mail-mime-parser
| 1 | <?php |
||||||
| 2 | /** |
||||||
| 3 | * This file is part of the ZBateson\MailMimeParser project. |
||||||
| 4 | * |
||||||
| 5 | * @license http://opensource.org/licenses/bsd-license.php BSD |
||||||
| 6 | */ |
||||||
| 7 | |||||||
| 8 | namespace ZBateson\MailMimeParser\Message; |
||||||
| 9 | |||||||
| 10 | use GuzzleHttp\Psr7\CachingStream; |
||||||
| 11 | use Psr\Http\Message\StreamInterface; |
||||||
| 12 | use Psr\Log\LoggerInterface; |
||||||
| 13 | use Psr\Log\LogLevel; |
||||||
| 14 | use ZBateson\MailMimeParser\ErrorBag; |
||||||
| 15 | use ZBateson\MailMimeParser\Stream\MessagePartStreamDecorator; |
||||||
| 16 | use ZBateson\MailMimeParser\Stream\StreamFactory; |
||||||
| 17 | use ZBateson\MbWrapper\MbWrapper; |
||||||
| 18 | use ZBateson\MbWrapper\UnsupportedCharsetException; |
||||||
| 19 | |||||||
| 20 | /** |
||||||
| 21 | * Holds the stream and content stream objects for a part. |
||||||
| 22 | * |
||||||
| 23 | * Note that streams are not explicitly closed or detached on destruction of the |
||||||
| 24 | * PartSreamContainer by design: the passed StreamInterfaces will be closed on |
||||||
| 25 | * their destruction when no references to them remain, which is useful when the |
||||||
| 26 | * streams are passed around. |
||||||
| 27 | * |
||||||
| 28 | * In addition, all the streams passed to PartStreamContainer should be wrapping |
||||||
| 29 | * a ZBateson\StreamDecorators\NonClosingStream unless attached to a part by a |
||||||
| 30 | * user, this is because MMP uses a single seekable stream for content and wraps |
||||||
| 31 | * it in ZBateson\StreamDecorators\SeekingLimitStream objects for each part. |
||||||
| 32 | * |
||||||
| 33 | * @author Zaahid Bateson |
||||||
| 34 | */ |
||||||
| 35 | class PartStreamContainer extends ErrorBag |
||||||
| 36 | { |
||||||
| 37 | /** |
||||||
| 38 | * @var MbWrapper to test charsets and see if they're supported. |
||||||
| 39 | */ |
||||||
| 40 | protected MbWrapper $mbWrapper; |
||||||
| 41 | |||||||
| 42 | /** |
||||||
| 43 | * @var bool if false, reading from a content stream with an unsupported |
||||||
| 44 | * charset will be tried with the default charset, otherwise the stream |
||||||
| 45 | * created with the unsupported charset, and an exception will be |
||||||
| 46 | * thrown when read from. |
||||||
| 47 | */ |
||||||
| 48 | protected bool $throwExceptionReadingPartContentFromUnsupportedCharsets; |
||||||
| 49 | |||||||
| 50 | /** |
||||||
| 51 | * @var StreamFactory used to apply psr7 stream decorators to the |
||||||
| 52 | * attached StreamInterface based on encoding. |
||||||
| 53 | */ |
||||||
| 54 | protected StreamFactory $streamFactory; |
||||||
| 55 | |||||||
| 56 | /** |
||||||
| 57 | * @var MessagePartStreamDecorator stream containing the part's headers, |
||||||
| 58 | * content and children wrapped in a MessagePartStreamDecorator |
||||||
| 59 | */ |
||||||
| 60 | protected MessagePartStreamDecorator $stream; |
||||||
| 61 | |||||||
| 62 | /** |
||||||
| 63 | * @var StreamInterface a stream containing this part's content |
||||||
| 64 | */ |
||||||
| 65 | protected ?StreamInterface $contentStream = null; |
||||||
| 66 | |||||||
| 67 | /** |
||||||
| 68 | * @var StreamInterface the content stream after attaching transfer encoding |
||||||
| 69 | * streams to $contentStream. |
||||||
| 70 | */ |
||||||
| 71 | protected ?StreamInterface $decodedStream = null; |
||||||
| 72 | |||||||
| 73 | /** |
||||||
| 74 | * @var StreamInterface attached charset stream to $decodedStream |
||||||
| 75 | */ |
||||||
| 76 | protected ?StreamInterface $charsetStream = null; |
||||||
| 77 | |||||||
| 78 | /** |
||||||
| 79 | * @var bool true if the stream should be detached when this container is |
||||||
| 80 | * destroyed. |
||||||
| 81 | */ |
||||||
| 82 | protected bool $detachParsedStream = false; |
||||||
| 83 | |||||||
| 84 | /** |
||||||
| 85 | * @var array<string, null> map of the active encoding filter on the current handle. |
||||||
| 86 | */ |
||||||
| 87 | private array $encoding = [ |
||||||
| 88 | 'type' => null, |
||||||
| 89 | 'filter' => null |
||||||
| 90 | ]; |
||||||
| 91 | |||||||
| 92 | /** |
||||||
| 93 | * @var array<string, null> map of the active charset filter on the current handle. |
||||||
| 94 | */ |
||||||
| 95 | private array $charset = [ |
||||||
| 96 | 'from' => null, |
||||||
| 97 | 'to' => null, |
||||||
| 98 | 'filter' => null |
||||||
| 99 | ]; |
||||||
| 100 | |||||||
| 101 | 117 | public function __construct( |
|||||
| 102 | LoggerInterface $logger, |
||||||
| 103 | StreamFactory $streamFactory, |
||||||
| 104 | MbWrapper $mbWrapper, |
||||||
| 105 | bool $throwExceptionReadingPartContentFromUnsupportedCharsets |
||||||
| 106 | ) { |
||||||
| 107 | 117 | parent::__construct($logger); |
|||||
| 108 | 117 | $this->streamFactory = $streamFactory; |
|||||
| 109 | 117 | $this->mbWrapper = $mbWrapper; |
|||||
| 110 | 117 | $this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets; |
|||||
| 111 | } |
||||||
| 112 | |||||||
| 113 | /** |
||||||
| 114 | * Sets the part's stream containing the part's headers, content, and |
||||||
| 115 | * children. |
||||||
| 116 | */ |
||||||
| 117 | 108 | public function setStream(MessagePartStreamDecorator $stream) : static |
|||||
| 118 | { |
||||||
| 119 | 108 | $this->stream = $stream; |
|||||
| 120 | 108 | return $this; |
|||||
| 121 | } |
||||||
| 122 | |||||||
| 123 | /** |
||||||
| 124 | * Returns the part's stream containing the part's headers, content, and |
||||||
| 125 | * children. |
||||||
| 126 | */ |
||||||
| 127 | 95 | public function getStream() : MessagePartStreamDecorator |
|||||
| 128 | { |
||||||
| 129 | // error out if called before setStream, getStream should never return |
||||||
| 130 | // null. |
||||||
| 131 | 95 | $this->stream->rewind(); |
|||||
| 132 | 95 | return $this->stream; |
|||||
| 133 | } |
||||||
| 134 | |||||||
| 135 | /** |
||||||
| 136 | * Returns true if there's a content stream associated with the part. |
||||||
| 137 | */ |
||||||
| 138 | 106 | public function hasContent() : bool |
|||||
| 139 | { |
||||||
| 140 | 106 | return ($this->contentStream !== null); |
|||||
| 141 | } |
||||||
| 142 | |||||||
| 143 | /** |
||||||
| 144 | * Attaches the passed stream as the content portion of this |
||||||
| 145 | * StreamContainer. |
||||||
| 146 | * |
||||||
| 147 | * The content stream would represent the content portion of $this->stream. |
||||||
| 148 | * |
||||||
| 149 | * If the content is overridden, $this->stream should point to a dynamic |
||||||
| 150 | * {@see ZBateson\Stream\MessagePartStream} that dynamically creates the |
||||||
| 151 | * RFC822 formatted message based on the IMessagePart this |
||||||
| 152 | * PartStreamContainer belongs to. |
||||||
| 153 | * |
||||||
| 154 | * setContentStream can be called with 'null' to indicate the IMessagePart |
||||||
| 155 | * does not contain any content. |
||||||
| 156 | */ |
||||||
| 157 | 114 | public function setContentStream(?StreamInterface $contentStream = null) : static |
|||||
| 158 | { |
||||||
| 159 | 114 | $this->contentStream = $contentStream; |
|||||
| 160 | 114 | $this->decodedStream = null; |
|||||
| 161 | 114 | $this->charsetStream = null; |
|||||
| 162 | 114 | return $this; |
|||||
| 163 | } |
||||||
| 164 | |||||||
| 165 | /** |
||||||
| 166 | * Returns true if the attached stream filter used for decoding the content |
||||||
| 167 | * on the current handle is different from the one passed as an argument. |
||||||
| 168 | */ |
||||||
| 169 | 83 | private function isTransferEncodingFilterChanged(?string $transferEncoding) : bool |
|||||
| 170 | { |
||||||
| 171 | 83 | return ($transferEncoding !== $this->encoding['type']); |
|||||
| 172 | } |
||||||
| 173 | |||||||
| 174 | /** |
||||||
| 175 | * Returns true if the attached stream filter used for charset conversion on |
||||||
| 176 | * the current handle is different from the one needed based on the passed |
||||||
| 177 | * arguments. |
||||||
| 178 | * |
||||||
| 179 | */ |
||||||
| 180 | 71 | private function isCharsetFilterChanged(string $fromCharset, string $toCharset) : bool |
|||||
| 181 | { |
||||||
| 182 | 71 | return ($fromCharset !== $this->charset['from'] |
|||||
| 183 | 71 | || $toCharset !== $this->charset['to']); |
|||||
| 184 | } |
||||||
| 185 | |||||||
| 186 | /** |
||||||
| 187 | * Attaches a decoding filter to the attached content handle, for the passed |
||||||
| 188 | * $transferEncoding. |
||||||
| 189 | */ |
||||||
| 190 | 113 | protected function attachTransferEncodingFilter(?string $transferEncoding) : static |
|||||
| 191 | { |
||||||
| 192 | 113 | if ($this->decodedStream !== null) { |
|||||
| 193 | 113 | $this->encoding['type'] = $transferEncoding; |
|||||
| 194 | 113 | $this->decodedStream = new CachingStream($this->streamFactory->getTransferEncodingDecoratedStream( |
|||||
| 195 | 113 | $this->decodedStream, |
|||||
| 196 | 113 | $transferEncoding |
|||||
| 197 | 113 | )); |
|||||
| 198 | } |
||||||
| 199 | 113 | return $this; |
|||||
| 200 | } |
||||||
| 201 | |||||||
| 202 | /** |
||||||
| 203 | * Attaches a charset conversion filter to the attached content handle, for |
||||||
| 204 | * the passed arguments. |
||||||
| 205 | * |
||||||
| 206 | * @param string $fromCharset the character set the content is encoded in |
||||||
| 207 | * @param string $toCharset the target encoding to return |
||||||
| 208 | */ |
||||||
| 209 | 92 | protected function attachCharsetFilter(string $fromCharset, string $toCharset) : static |
|||||
| 210 | { |
||||||
| 211 | 92 | if ($this->charsetStream !== null) { |
|||||
| 212 | 92 | if (!$this->throwExceptionReadingPartContentFromUnsupportedCharsets) { |
|||||
| 213 | try { |
||||||
| 214 | 91 | $this->mbWrapper->convert('t', $fromCharset, $toCharset); |
|||||
| 215 | 90 | $this->charsetStream = new CachingStream($this->streamFactory->newCharsetStream( |
|||||
| 216 | 90 | $this->charsetStream, |
|||||
| 217 | 90 | $fromCharset, |
|||||
| 218 | 90 | $toCharset |
|||||
| 219 | 90 | )); |
|||||
| 220 | 1 | } catch (UnsupportedCharsetException $ex) { |
|||||
| 221 | 1 | $this->addError('Unsupported character set found', LogLevel::ERROR, $ex); |
|||||
| 222 | 1 | $this->charsetStream = new CachingStream($this->charsetStream); |
|||||
| 223 | } |
||||||
| 224 | } else { |
||||||
| 225 | 1 | $this->charsetStream = new CachingStream($this->streamFactory->newCharsetStream( |
|||||
| 226 | 1 | $this->charsetStream, |
|||||
| 227 | 1 | $fromCharset, |
|||||
| 228 | 1 | $toCharset |
|||||
| 229 | 1 | )); |
|||||
| 230 | } |
||||||
| 231 | 92 | $this->charsetStream->rewind(); |
|||||
| 232 | 92 | $this->charset['from'] = $fromCharset; |
|||||
| 233 | 92 | $this->charset['to'] = $toCharset; |
|||||
| 234 | } |
||||||
| 235 | 92 | return $this; |
|||||
| 236 | } |
||||||
| 237 | |||||||
| 238 | /** |
||||||
| 239 | * Resets just the charset stream, and rewinds the decodedStream. |
||||||
| 240 | */ |
||||||
| 241 | 92 | private function resetCharsetStream() : static |
|||||
| 242 | { |
||||||
| 243 | 92 | $this->charset = [ |
|||||
| 244 | 92 | 'from' => null, |
|||||
| 245 | 92 | 'to' => null, |
|||||
| 246 | 92 | 'filter' => null |
|||||
| 247 | 92 | ]; |
|||||
| 248 | 92 | $this->decodedStream->rewind(); |
|||||
|
0 ignored issues
–
show
|
|||||||
| 249 | 92 | $this->charsetStream = $this->decodedStream; |
|||||
| 250 | 92 | return $this; |
|||||
| 251 | } |
||||||
| 252 | |||||||
| 253 | /** |
||||||
| 254 | * Resets cached encoding and charset streams, and rewinds the stream. |
||||||
| 255 | */ |
||||||
| 256 | 113 | public function reset() : static |
|||||
| 257 | { |
||||||
| 258 | 113 | $this->encoding = [ |
|||||
| 259 | 113 | 'type' => null, |
|||||
| 260 | 113 | 'filter' => null |
|||||
| 261 | 113 | ]; |
|||||
| 262 | 113 | $this->charset = [ |
|||||
| 263 | 113 | 'from' => null, |
|||||
| 264 | 113 | 'to' => null, |
|||||
| 265 | 113 | 'filter' => null |
|||||
| 266 | 113 | ]; |
|||||
| 267 | 113 | $this->contentStream->rewind(); |
|||||
|
0 ignored issues
–
show
The method
rewind() does not exist on null.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 268 | 113 | $this->decodedStream = $this->contentStream; |
|||||
| 269 | 113 | $this->charsetStream = $this->contentStream; |
|||||
| 270 | 113 | return $this; |
|||||
| 271 | } |
||||||
| 272 | |||||||
| 273 | /** |
||||||
| 274 | * Checks what transfer-encoding decoder stream and charset conversion |
||||||
| 275 | * stream are currently attached on the underlying contentStream, and resets |
||||||
| 276 | * them if the requested arguments differ from the currently assigned ones. |
||||||
| 277 | * |
||||||
| 278 | * @param IMessagePart $part the part the stream belongs to |
||||||
| 279 | * @param string $transferEncoding the transfer encoding |
||||||
| 280 | * @param string $fromCharset the character set the content is encoded in |
||||||
| 281 | * @param string $toCharset the target encoding to return |
||||||
| 282 | */ |
||||||
| 283 | 112 | public function getContentStream( |
|||||
| 284 | IMessagePart $part, |
||||||
| 285 | ?string $transferEncoding, |
||||||
| 286 | ?string $fromCharset, |
||||||
| 287 | ?string $toCharset |
||||||
| 288 | ) : ?MessagePartStreamDecorator { |
||||||
| 289 | 112 | if ($this->contentStream === null) { |
|||||
| 290 | 2 | return null; |
|||||
| 291 | } |
||||||
| 292 | 111 | if (empty($fromCharset) || empty($toCharset)) { |
|||||
| 293 | 72 | return $this->getBinaryContentStream($part, $transferEncoding); |
|||||
| 294 | } |
||||||
| 295 | 92 | if ($this->charsetStream === null |
|||||
| 296 | 71 | || $this->isTransferEncodingFilterChanged($transferEncoding) |
|||||
| 297 | 92 | || $this->isCharsetFilterChanged($fromCharset, $toCharset)) { |
|||||
| 298 | 92 | if ($this->charsetStream === null |
|||||
| 299 | 92 | || $this->isTransferEncodingFilterChanged($transferEncoding)) { |
|||||
| 300 | 92 | $this->reset(); |
|||||
| 301 | 92 | $this->attachTransferEncodingFilter($transferEncoding); |
|||||
| 302 | } |
||||||
| 303 | 92 | $this->resetCharsetStream(); |
|||||
| 304 | 92 | $this->attachCharsetFilter($fromCharset, $toCharset); |
|||||
| 305 | } |
||||||
| 306 | 92 | $this->charsetStream->rewind(); |
|||||
|
0 ignored issues
–
show
The method
rewind() does not exist on null.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 307 | 92 | return $this->streamFactory->newDecoratedMessagePartStream( |
|||||
| 308 | 92 | $part, |
|||||
| 309 | 92 | $this->charsetStream |
|||||
|
0 ignored issues
–
show
It seems like
$this->charsetStream can also be of type null; however, parameter $stream of ZBateson\MailMimeParser\...atedMessagePartStream() does only seem to accept Psr\Http\Message\StreamInterface, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 310 | 92 | ); |
|||||
| 311 | } |
||||||
| 312 | |||||||
| 313 | /** |
||||||
| 314 | * Checks what transfer-encoding decoder stream is attached on the |
||||||
| 315 | * underlying stream, and resets it if the requested arguments differ. |
||||||
| 316 | */ |
||||||
| 317 | 75 | public function getBinaryContentStream(IMessagePart $part, ?string $transferEncoding = null) : ?MessagePartStreamDecorator |
|||||
| 318 | { |
||||||
| 319 | 75 | if ($this->contentStream === null) { |
|||||
| 320 | 1 | return null; |
|||||
| 321 | } |
||||||
| 322 | 74 | if ($this->decodedStream === null |
|||||
| 323 | 74 | || $this->isTransferEncodingFilterChanged($transferEncoding)) { |
|||||
| 324 | 74 | $this->reset(); |
|||||
| 325 | 74 | $this->attachTransferEncodingFilter($transferEncoding); |
|||||
| 326 | } |
||||||
| 327 | 74 | $this->decodedStream->rewind(); |
|||||
| 328 | 74 | return $this->streamFactory->newDecoratedMessagePartStream($part, $this->decodedStream); |
|||||
| 329 | } |
||||||
| 330 | |||||||
| 331 | 1 | protected function getErrorBagChildren() : array |
|||||
| 332 | { |
||||||
| 333 | 1 | return []; |
|||||
| 334 | } |
||||||
| 335 | } |
||||||
| 336 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.