Is it normal that a data flow task's OnError event handler fires 3x and how to avoid that?
In my SSIS packages, I enclose the entire control flow in a sequence container, with a "Send Mail" task of the container's "on failure" flow.

In my error handlers, I collect the error information and append it to a string variable.

Then, in the "Send Mail" task, I include the string with the accumulated error information in the body of the message.

Now, this only works as is if the failure in your data flow is fatal to the package as a whole. If you're (for example) processing several files, and want to send out one error message for each file that fails its processing, you'd need to modify this to send the mail at the correct time, and clear the error detail variable; however, the same principle should apply.