Salesforce batch class is designed in a way that
start(Database.BatchableContext bc) and
finish(Database.BatchableContext BC) runs only once in the entire context of batch class run. It is the
execute(Database.BatchableContext BC, list<sObject>) which runs multiple times based on batch size and total number of records returned from start method and each run of the execute method has it's own order of execution and gets reset after every run.
For example: when a batch runs let suppose the batch size is 300 and total number of records returned from start method is 700.
So number of times execute method will run = 700/ 300 = 2.33 = 3 times (Rounded of to next high value, since batch cannot run in decimal context i.e either batch is processing all values corresponds to batch size or processing records less than < batch size)
so 1st execute method run(it completes one full salesforce order of execution )--->2nd execute start ( it completes one full salesforce order of execution which is not linked to 1st batch execution) ---> similarly 3rd execute runs
If your email is getting sent from finish method, there is no way it will fire multiple times in a single batch run context.
From the comment we can conclude since the batch is getting fired from trigger and trigger runs on batch of 200 records (Please refer documentation for more information)
Beyond the Basics
Triggers execute on batches of 200 records at a
time. So if 400 records cause a trigger to fire, the trigger fires
twice, once for each 200 records. For this reason, you don’t get the
benefit of SOQL for loop record batching in triggers, because triggers
batch up records as well. The SOQL for loop is called twice in this
example, but a standalone SOQL query would also be called twice.
However, the SOQL for loop still looks more elegant than iterating
over a collection variable!
Therefore batch's finish method is getting triggered multiple times and hence you are receiving multiple emails