Assuming that you will always have access to at least one contact of the Database, there is a blog post in https://appirio.com/tech-blog/sending-emails-without-targetobject-id-apex that shows a work-around to do so.
Bottom line, it uses a random contact to apply the template on the call of sendEmail
, which is not truly sent because it does a rollback. The point is that by that time, the HTML, subject and plain text of the email is already merged, so it can be used to send the real email.
Id whatID = 'xxxxxxxxxxxxxxxxxx';
Contact cnt = [select id, Email from Contact where email != null limit 1];
List<Messaging.SingleEmailMessage> msgList= new List<Messaging.SingleEmailMessage>();
Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
msg.setTemplateId('yyyyyyyyyyyyyyyyyy');
msg.setWhatId(whatID);
msg.setTargetObjectId(cnt.id);
msg.setToAddresses(new List<String>{'your@example.com'});
msgList.add(msg);
// Send the emails in a transaction, then roll it back
Savepoint sp = Database.setSavepoint();
Messaging.sendEmail(msgList); // Dummy email send
Database.rollback(sp); // Email will not send as it is rolled Back
// Send Actual email
List<Messaging.SingleEmailMessage> msgListToBeSend = new List<Messaging.SingleEmailMessage>();
for (Messaging.SingleEmailMessage email : msgList) {
Messaging.SingleEmailMessage emailToSend = new Messaging.SingleEmailMessage();
emailToSend.setToAddresses(email.getToAddresses());
emailToSend.setPlainTextBody(email.getPlainTextBody());
emailToSend.setHTMLBody(email.getHTMLBody());
emailToSend.setSubject(email.getSubject());
msgListToBeSend.add(emailToSend);
}
Messaging.sendEmail(msgListToBeSend);
Most likely you will have to test this and adapt it a bit to your needs, in case you send multiple emails, etc. But I hope you get the idea behind.