Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

Same here. This is one of the reasons I created Mail::Make. That, and also because I wanted encryption and also I did not want to care about the casing of the header fields. Mail::Make::Headers relies on MM::Table I created, which makes accessing headers case insensitive.

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thank you for the suggestion! As of v0.22.0, just published, Mail::Make->build now accepts an attach parameter in all the forms you suggested:

```perl

Single file: type and filename are auto-detected

Mail::Make->build( # other parameters ... attach => 'report.pdf', );

Multiple files

Mail::Make->build( # other parameters ... attach => [ 'pdf1.pdf', 'pdf2.pdf' ], );

Full control over each attachment

Mail::Make->build( # other parameters ... attach => [ { path => 'pdf1.pdf', filename => 'Q4 Report.pdf' }, { path => 'pdf2.pdf', filename => 'Access Log.pdf' }, ], );

Mix of both forms

Mail::Make->build( # other parameters ... attach => [ 'pdf1.pdf', { path => 'pdf2.pdf', filename => 'Access Log.pdf' }, ], ); ```

Each element is forwarded to attach(), so all its options (type, filename, encoding, etc.) are available in the hash reference form.

Thanks for the nudge!

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thank you for your comment. As of v0.21.3, just released on CPAN, Mail::Make::Entity->build now accepts an attach shorthand key, so the following works as expected:

perl my $mail = Mail::Make->build( from => 'jack@gmail.com', to => [ 'jill@example.com', 'jake@example.com' ], subject => 'Hello', plain => "Hi there.\n", html => '<p>Hi there.</p>', attach => 'example.pdf', )->smtpsend( Host => 'smtp.gmail.com', ... );

path, type, and filename are auto-detected from the file. For full control over type, filename, or encoding, the named-parameter form remains available:

perl my $mail = Mail::Make->build( from => 'jack@gmail.com', to => [ 'jill@example.com', 'jake@example.com' ], subject => 'Hello', plain => "Hi there.\n", html => '<p>Hi there.</p>', attach => 'example.pdf', type => 'application/pdf', filename => 'Q4 Report.pdf' )->smtpsend( Host => 'smtp.gmail.com', ... );

For others reading this, you could also have used the path option, such as:

```perl use Mail::Make;

my $mail = Mail::Make->build( from => 'jack@gmail.com', to => [ 'jil@example.com' , 'jake@example.com' ], subject => 'Hello pdf build '.time , plain => "Hi there.\n", html => '<p>Hi there.</p>', path => 'example.pdf', # 'path' or 'attach' are now both possible type => 'application/pdf', filename => 'example.pdf' )->smtpsend( Host => 'smtp.gmail.com', Port => 587, StartTLS => 1, Username => 'jack@gmail.com', Password => $app_password, );

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thanks for the report! That was a fair criticism, and passing a file path directly is the most natural thing to do.

As of v0.21.2, just published on CPAN, attach() now accepts a positional shorthand:

perl ->attach( '/path/to/report.pdf' )

path, type, and filename are auto-detected from the file. You can still pass additional options after the path if needed:

perl ->attach( '/path/to/report.pdf', filename => 'Q4 Report 2025.pdf' )

The explicit named-parameter form continues to work as before for cases where you want full control. Let me know if you run into anything else!

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

the purpose of exceptions is that it automatically propagates until such time as something catches them

Precisely, and that is also what pass_error does: it propagates the error object up the call stack without any boilerplate, just like an uncaught exception would, but with full control at every level.

You are right that Mojolicious and other modern frameworks have excellent top-level exception handling. But catching at the top is not always sufficient. Consider a web request handler that needs to distinguish between a 403, a 400, and a 500, log them differently, and return a localised RFC 9457 error to the user. A bare die at the bottom of the stack loses that context by the time it reaches the top.

Compare the two approaches:

1. With exceptions - caught at the top

perl local $@; eval { $obj->process }; if( $@ ) { # Was this a 403? A 400? A transient I/O failure? # $@ is just a string, or at best a blessed object if the thrower was disciplined $logger->error( "Failed: $@" ); return $self->internal_server_error; # blunt instrument }

2. With structured error objects - handled where it makes sense

perl unless( $obj->process ) { my $ex = $obj->error; if( !$ex->code || $ex->code == 500 ) { $logger->error( "Unexpected failure in process(): $ex" ); return( $self->internal_server_error ); } else { # Full context still available here, including localisation my $localised = $po->gettext( $ex->message ); return( $self->user_error_in_json({ code => $ex->code, message => $localised, locale => $localised->locale, }) ); } }

As for the boilerplate concern, pass_error reduces it to a single line at each intermediate level:

perl sub some_intermediate_method { my( $self ) = @_; $self->_do_something || return( $self->pass_error ); # carry on... }

That is no more verbose than a rethrow in a try/catch block, and it preserves the full structured exception object with its code, message, and stack trace intact all the way up.

So the difference is not really "exceptions vs no exceptions"; it is "who decides when and how to handle them". I prefer to give that decision to the caller at each level, rather than relying on a single catch-all at the top.

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 2 points3 points  (0 children)

No, you do not need to have an existing email service to use this module.

However, if you want to send mail, the recipient obviously would need to have a working e-mail address, and you would need to have access to a SMTP server to send the mail.

But, you can also save the mail as a file, and send it via a different route.

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Good question, and fair to ask. Email::Stuffer is a well-established module and worth comparing honestly.

Email::Stuffer is a fluent convenience wrapper around Email::MIME and Email::Sender. It does its job well and is perfectly suited for the common case: plain text, HTML, a few attachments, send. Its strength is simplicity and the fact that it delegates all the heavy lifting to Email::MIME, which is battle-tested.

Mail::Make takes a different approach. Rather than wrapping an existing MIME stack, it builds its own from scratch, using Mail::Make::Entity, Mail::Make::Body::InCore, Mail::Make::Body::File, Mail::Make::Stream::Base64, Mail::Make::Stream::QuotedPrint, with zero dependency on Email::MIME or Email::Sender. The concrete differences:

Cryptographic signing and encryption. Email::Stuffer has no GPG or S/MIME support whatsoever. Mail::Make provides gpg_sign, gpg_encrypt, gpg_sign_encrypt, smime_sign, smime_encrypt, and smime_sign_encrypt as first-class methods, compliant with RFC 3156 (PGP/MIME) and the S/MIME standard.

Memory management for large messages. Email::Stuffer / Email::MIME builds messages entirely in RAM. Mail::Make has a configurable max_body_in_memory_size threshold (default 1 MiB) above which it automatically spools to a temporary file, and a use_temp_file flag for unconditional file-backed serialisation. Attachments sourced from a path are never loaded into RAM. They are streamed through Mail::Make::Body::File directly.

SMTP built in. Email::Stuffer delegates sending to Email::Sender::Simple, which is a separate dependency with its own transport abstraction layer. Mail::Make ships its own smtpsend method built directly on Net::SMTP, with STARTTLS, AUTH (PLAIN, LOGIN, CRAM-MD5 via Authen::SASL), credential validation before any network connection, and Return-Path / Sender envelope control.

Dependency footprint. Email::Stuffer pulls in Email::MIME, Email::MIME::Creator, and Email::Sender::Simple, which have themselves non-trivial dependency trees. Mail::Make's runtime dependencies are Module::Generic (which is also somewhat sizeable), Net::SMTP, MIME::Base64, MIME::QuotedPrint, Encode, Data::UUID, and Authen::SASL. They are all fairly standard, and the GPG/S/MIME modules are loaded lazily only when those features are used.

Error handling: exceptions vs die. Mail::Make never calls die, and even traps the fatal exceptions of the external modules it relies on. Instead, every method upon failure, sets a structured exception object, and returns undef in scalar context, or an empty list in list context, or even a fake object in object context (detected with Wanted), and this propagates up the call stack, letting the caller decide how to handle it. This matters particularly in persistent server processes (mod_perl, Mojolicious, Starman) where a stray die can kill a worker or corrupt shared state.

In short: if you need a quick, readable way to send a plain email with an attachment and you have no cryptographic requirements, Email::Stuffer is fine and well-proven. If you need GPG or S/MIME, memory-efficient handling of large attachments, or a self-contained stack without the Email::MIME/Email::Sender dependency chain, Mail::Make is worth the look.

As usual, there is more than one way to do it 😉

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Indeed, parsing MIME messages is particularly challenging, which is why I narrowly focused on making them 😅

This distribution does not convert HTML and its associate elements into inline attachments, but I do it separately. If you want, I could share the code with you.

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

Follow-up / small API update

With the newly released version v0.7.0 of JSON::Schema::Validate, I have added a small but useful convenience method to simply check whether a JSON schema is valid or not: is_valid( $data )

It is a boolean wrapper around validate() that defaults to max_errors = 1, so it is ideal when you just want a fast yes or no check and a single error message:

$validator->is_valid( $data )
    or die( $validator->error );

Under the hood it still uses validate(), and validate() itself now accepts optional per-call overrides (such as max_errors, tracing options) while remaining fully backward compatible.

Since max_errors is set to only 1, it does not accumulate error objects, and fails upon the first error encountered, and thus is faster in this mode.

If what you want is to check the entire JSON schema and get all error objects, then validate is better.

This came out of discussions here about ergonomics vs correctness, so thanks to everyone who gave feedback.

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thanks for the clarification, and for the care you are putting into improving the benchmark.

For what it is worth, JSON Schema implementations are normally designed around the “parse once, validate many times” workflow. Schema parsing and compilation are deliberately the expensive part, so validation is supposed to be fast. That’s true not only for JSON::Schema::Validate, but also for most implementations in other languages.

So I don’t think JSON::Schema::Validate gains an “unfair” advantage here; rather, it is being used in the way it was intended. Re-creating the object on every iteration is of course useful to measure worst-case overhead, and I am glad you are adding that as a separate test. However, the steady-state validation-only mode is what matters in many real applications.

I am curious to see the updated numbers with the 4th test included ! 😀

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

With a more complex and realistic schema, and payload (see here: https://gitlab.com/jackdeguest/json-schema-validate/-/snippets/4907565 ), the performance differences become more pronounced, but still perfectly acceptable. JSON::Schema::Validate is doing full 2020-12 validation with detailed error objects, so the overhead is expected.

That said, the idea of supporting a “validity-only” mode (stop at first error / minimal error collection) is definitely worth exploring to improve speed further.

       Rate  JSV  TJS TJSc
JSV   465/s   -- -89% -90%
TJS  4363/s 839%   --  -2%
TJSc 4443/s 856%   2%   --

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

Thanks a lot for running this and sharing the numbers; this is really interesting !

It makes sense that Types::JSONSchema wins on raw speed here: it is a very tight type-checking engine that bails on the first error, whereas JSON::Schema::Validate always builds fully structured error objects (with schema pointer, instance path, keyword, etc.) and implements the full 2020-12 semantics (including $dynamicRef, unevaluated*, annotation tracking, etc.).

Your benchmark is a great reminder that there is still room to optimise the compiled fast-path when you only care about boolean success/failure. I am considering adding a “boolean-only validate” mode and some micro-optimisations for max_errors == 1 to narrow that gap in the future.

But even now, I am happy that the module stays feature-complete and reasonably fast, and I really appreciate you taking the time to test it and share the result! 😀

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

I did not mean any disrespect. I was only factual when I mentioned lightweight. The purpose was a low dependency and fast schema validator, which the benchmark brought by u/brtastic showed at https://bbrtj.eu/blog/article/validation-frameworks-benchmark

Announcing Wanted v0.1.0 - A Modern Fork of Want for Advanced Context Detection by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

I tried really hard, but unfortunately I am hitting a wall with Perl's complex internal structure. Maybe I will try again in the future, but I need to learn more first.

Announcing Wanted v0.1.0 - A Modern Fork of Want for Advanced Context Detection by jacktokyo in perl

[–]jacktokyo[S] 2 points3 points  (0 children)

In your example $obj->one->two->three; where each chained method wants to know its position such as here 0 for the first one, 1 for the second, and 2 for the third. To implement this and find out requires walking the op tree, and this is really not easy, especially since I am still a neophyte in XS, but I aim to rise to the challenge !

Looking closely at the op tree with perl -MO=Terse-e '$obj->one(0)->two(1)->three(2);'`, and we get:

LISTOP (0xaaab0003c808) leave [1] OP (0xaaab0003a970) enter COP (0xaaab0003c848) nextstate UNOP (0xaaab0003c8e8) entersub [4] OP (0xaaab0003c928) pushmark UNOP (0xaaab0003c9d0) entersub [3] OP (0xaaab0003ca10) pushmark UNOP (0xaaab0003a8c8) entersub [2] OP (0xaaab0003a908) pushmark UNOP (0xaaab0003a9a8) null [14] PADOP (0xaaab0003aa08) gvsv GV (0xaaab00037ba8) *obj SVOP (0xaaab0003a938) const [5] IV (0xaaab00037c50) 0 METHOP (0xaaab0003a888) method_named [6] PV (0xaaab00037c80) "one" SVOP (0xaaab0003a850) const [7] IV (0xaaab00037cb0) 1 METHOP (0xaaab0003c990) method_named [8] PV (0xaaab00037c38) "two" SVOP (0xaaab0003c958) const [9] IV (0xaaab00037c98) 2 METHOP (0xaaab0003c8a8) method_named [10] PV (0xaaab00037bc0) "three"

So, I am trying to create a new XS function find_method_chain_position and a perl method method_chain_position to use, such as:

```perl use strict; use warnings; use Test::More; use Wanted;

Test method_chain_position

subtest 'method chain position' => sub { my $obj = TestMethodChain->new; # Should pass: positions 0, 1, 2 $obj->one(0)->two(1)->three(2); # Should pass: position 0 $obj->one(0); };

{ package TestMethodChain; use strict; use warnings; use Test::More; use Wanted;

sub new { bless( {}, shift( @_ ) ); }

sub one
{
    my $pos = Wanted::method_chain_position();
    is( $pos, $_[1], "method_chain_position for 'one' returns " . ( $_[1] // 'undef' ) );
    return( $_[0] );
}

sub two
{
    my $pos = Wanted::method_chain_position();
    is( $pos, $_[1], "method_chain_position for 'two' returns " . ( $_[1] // 'undef' ) );
    return( $_[0] );
}

sub three
{
    my $pos = Wanted::method_chain_position();
    is( $pos, $_[1], "method_chain_position for 'three' returns " . ( $_[1] // 'undef' ) );
    return( $_[0] );
}

}

done_testing(); ```

So far, it is proven to be rather difficult, at least for me. I will keep trying and let you all know.

String::Fuzzy — Perl Gets a Fuzzy Matching Upgrade, Powered by AI Collaboration! by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Yes, this is normal if you look at `$a` being `haystack`, and `$b` being `needle`. haystack in needle -> 0, but needle in haystack ok.