module MIME::Multipart # Builds a multipart MIME message. # # ### Example # # ``` # require "mime/multipart" # # io = IO::Memory.new # This is a stub. Actually, any IO can be used. # multipart = MIME::Multipart::Builder.new(io) # multipart.body_part HTTP::Headers{"Content-Type" => "text/plain"}, "hello!" # multipart.finish # io.to_s # => "----------------------------DTf61dRTHYzprx7rwVQhTWr7\r\nContent-Type: text/plain\r\n\r\nhello!\r\n----------------------------DTf61dRTHYzprx7rwVQhTWr7--" # ``` class Builder # Creates a new `Multipart::Builder` which writes the generated multipart # message to *io*, using the multipart boundary *boundary*. def initialize(@io : IO, @boundary = Multipart.generate_boundary) @state = State::START end getter boundary # Finite State Machine diagram: https://gist.github.com/RX14/221c1edfa98d1196711515d4b5c264eb # Returns a content type header with multipart subtype *subtype*, and # boundary parameter added. # # ``` # require "mime/multipart" # # io = IO::Memory.new # This is a stub. Actually, any IO can be used. # builder = MIME::Multipart::Builder.new(io, "a4VF") # builder.content_type("mixed") # => "multipart/mixed; boundary=a4VF" # ``` def content_type(subtype : String = "mixed") : String MIME::MediaType.new("multipart/#{subtype}", {"boundary" => @boundary}).to_s end # Appends *string* to the preamble segment of the multipart message. Throws # if `#body_part` is called before this method. # # Can be called multiple times to append to the preamble multiple times. def preamble(string : String) : Nil preamble { |io| string.to_s(io) } end # Appends *data* to the preamble segment of the multipart message. Throws # if `#body_part` is called before this method. # # Can be called multiple times to append to the preamble multiple times. def preamble(data : Bytes) : Nil preamble(&.write(data)) end # Appends *preamble_io* to the preamble segment of the multipart message. # Throws if `#body_part` is called before this method. # # Can be called multiple times to append to the preamble multiple times. def preamble(preamble_io : IO) : Nil preamble { |io| IO.copy(preamble_io, io) } end # Yields an IO that can be used to append to the preamble of the multipart # message. Throws if `#body_part` is called before this method. # # Can be called multiple times to append to the preamble multiple times. def preamble(&) fail "Cannot generate preamble: body already started" unless @state.start? || @state.preamble? yield @io @state = State::PREAMBLE end # Appends a body part to the multipart message with the given *headers* # and *string*. Throws if `#finish` or `#epilogue` is called before this # method. def body_part(headers : HTTP::Headers, string : String) : Nil body_part_impl(headers) { |io| string.to_s(io) } end # Appends a body part to the multipart message with the given *headers* # and *data*. Throws if `#finish` or `#epilogue` is called before this # method. def body_part(headers : HTTP::Headers, data : Bytes) : Nil body_part_impl(headers, &.write(data)) end # Appends a body part to the multipart message with the given *headers* # and data from *body_io*. Throws if `#finish` or `#epilogue` is called # before this method. def body_part(headers : HTTP::Headers, body_io : IO) : Nil body_part_impl(headers) { |io| IO.copy(body_io, io) } end # Yields an IO that can be used to write to a body part which is appended # to the multipart message with the given *headers*. Throws if `#finish` or # `#epilogue` is called before this method. def body_part(headers : HTTP::Headers, &) body_part_impl(headers) { |io| yield io } end # Appends a body part to the multipart message with the given *headers* # and no body data. Throws is `#finish` or `#epilogue` is called before # this method. def body_part(headers : HTTP::Headers) : Nil body_part_impl(headers, empty: true) { } end private def body_part_impl(headers, empty = false, &) fail "Cannot generate body part: already finished" if @state.finished? fail "Cannot generate body part: after epilogue" if @state.epilogue? # We don't add a crlf before the first boundary if this is the first body # part and there is no preamble @io << "\r\n" unless @state.start? @io << "--" << @boundary headers.each do |name, values| values.each do |value| @io << "\r\n" << name << ": " << value end end @io << "\r\n\r\n" unless empty yield @io @state = State::BODY_PART end # Appends *string* to the epilogue segment of the multipart message. Throws # if `#finish` is called before this method, or no body parts have been # appended. # # Can be called multiple times to append to the epilogue multiple times. def epilogue(string : String) : Nil epilogue { |io| string.to_s(io) } end # Appends *data* to the epilogue segment of the multipart message. Throws # if `#finish` is called before this method, or no body parts have been # appended. # # Can be called multiple times to append to the epilogue multiple times. def epilogue(data : Bytes) : Nil epilogue(&.write(data)) end # Appends *preamble_io* to the epilogue segment of the multipart message. # Throws if `#finish` is called before this method, or no body parts have # been appended. # # Can be called multiple times to append to the epilogue multiple times. def epilogue(epilogue_io : IO) : Nil epilogue { |io| IO.copy(epilogue_io, io) } end # Yields an IO that can be used to append to the epilogue of the multipart # message. Throws if `#finish` is called before this method, or no body # parts have been appended. # # Can be called multiple times to append to the preamble multiple times. def epilogue(&) case @state in .start?, .preamble? fail "Cannot generate epilogue: no body parts" in .finished? fail "Cannot generate epilogue: already finished" in .epilogue? # do nothing in .body_part? # We need to send the end boundary @io << "\r\n--" << @boundary << "--\r\n" in .errored? fail "BUG: unexpected error state" end yield @io @state = State::EPILOGUE end # Finalizes the multipart message, this method must be called to properly # end the multipart message. def finish : Nil case @state in .start?, .preamble? fail "Cannot finish multipart: no body parts" in .finished? fail "Cannot finish multipart: already finished" in .body_part? # We need to send the end boundary @io << "\r\n--" << @boundary << "--" in .epilogue? # do nothing in .errored? fail "BUG: unexpected error state" end @state = State::FINISHED @io.flush end private def fail(msg) raise Multipart::Error.new msg end end end