1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-mail for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Mail\Storage\Part;
10
11use Laminas\Mail\Headers;
12use Laminas\Mail\Storage\Part;
13
14class File extends Part
15{
16    protected $contentPos = [];
17    protected $partPos = [];
18    protected $fh;
19
20    /**
21     * Public constructor
22     *
23     * This handler supports the following params:
24     * - file     filename or open file handler with message content (required)
25     * - startPos start position of message or part in file (default: current position)
26     * - endPos   end position of message or part in file (default: end of file)
27     * - EOL      end of Line for messages
28     *
29     * @param   array $params  full message with or without headers
30     * @throws Exception\RuntimeException
31     * @throws Exception\InvalidArgumentException
32     */
33    public function __construct(array $params)
34    {
35        if (empty($params['file'])) {
36            throw new Exception\InvalidArgumentException('no file given in params');
37        }
38
39        if (! is_resource($params['file'])) {
40            $this->fh = fopen($params['file'], 'r');
41        } else {
42            $this->fh = $params['file'];
43        }
44        if (! $this->fh) {
45            throw new Exception\RuntimeException('could not open file');
46        }
47        if (isset($params['startPos'])) {
48            fseek($this->fh, $params['startPos']);
49        }
50        $header = '';
51        $endPos = isset($params['endPos']) ? $params['endPos'] : null;
52        while (($endPos === null || ftell($this->fh) < $endPos) && trim($line = fgets($this->fh))) {
53            $header .= $line;
54        }
55
56        if (isset($params['EOL'])) {
57            $this->headers = Headers::fromString($header, $params['EOL']);
58        } else {
59            $this->headers = Headers::fromString($header);
60        }
61
62        $this->contentPos[0] = ftell($this->fh);
63        if ($endPos !== null) {
64            $this->contentPos[1] = $endPos;
65        } else {
66            fseek($this->fh, 0, SEEK_END);
67            $this->contentPos[1] = ftell($this->fh);
68        }
69        if (! $this->isMultipart()) {
70            return;
71        }
72
73        $boundary = $this->getHeaderField('content-type', 'boundary');
74        if (! $boundary) {
75            throw new Exception\RuntimeException('no boundary found in content type to split message');
76        }
77
78        $part = [];
79        $pos = $this->contentPos[0];
80        fseek($this->fh, $pos);
81        while (! feof($this->fh) && ($endPos === null || $pos < $endPos)) {
82            $line = fgets($this->fh);
83            if ($line === false) {
84                if (feof($this->fh)) {
85                    break;
86                }
87                throw new Exception\RuntimeException('error reading file');
88            }
89
90            $lastPos = $pos;
91            $pos = ftell($this->fh);
92            $line = trim($line);
93
94            if ($line == '--' . $boundary) {
95                if ($part) {
96                    // not first part
97                    $part[1] = $lastPos;
98                    $this->partPos[] = $part;
99                }
100                $part = [$pos];
101            } elseif ($line == '--' . $boundary . '--') {
102                $part[1] = $lastPos;
103                $this->partPos[] = $part;
104                break;
105            }
106        }
107        $this->countParts = count($this->partPos);
108    }
109
110    /**
111     * Body of part
112     *
113     * If part is multipart the raw content of this part with all sub parts is returned
114     *
115     * @param resource $stream Optional
116     * @return string body
117     */
118    public function getContent($stream = null)
119    {
120        fseek($this->fh, $this->contentPos[0]);
121        if ($stream !== null) {
122            return stream_copy_to_stream($this->fh, $stream, $this->contentPos[1] - $this->contentPos[0]);
123        }
124        $length = $this->contentPos[1] - $this->contentPos[0];
125        return $length < 1 ? '' : fread($this->fh, $length);
126    }
127
128    /**
129     * Return size of part
130     *
131     * Quite simple implemented currently (not decoding). Handle with care.
132     *
133     * @return int size
134     */
135    public function getSize()
136    {
137        return $this->contentPos[1] - $this->contentPos[0];
138    }
139
140    /**
141     * Get part of multipart message
142     *
143     * @param  int $num number of part starting with 1 for first part
144     * @throws Exception\RuntimeException
145     * @return Part wanted part
146     */
147    public function getPart($num)
148    {
149        --$num;
150        if (! isset($this->partPos[$num])) {
151            throw new Exception\RuntimeException('part not found');
152        }
153
154        return new static(['file' => $this->fh, 'startPos' => $this->partPos[$num][0],
155                              'endPos' => $this->partPos[$num][1]]);
156    }
157}
158