There isn't a simple catch-all solution. You can't reference OpportunityContactRoles in a Workflow email alert so you have to use a custom Contact lookup on the Opportunity (for example, Opportunity.Primary_OCR__c). There are a couple of ways to automate the population of this lookup but neither is foolproof.
Solution 1:
You can create a before update trigger on the Opportunity which queries OpportunityContactRole where IsPrimary = true. Be sure to bulkify your trigger and map Opportunity Ids to the located OpportunityContactRole.
There is a serious limitation to this approach, however, as it requires you to update the Opportunity record itself before it will update the Primary OCR lookup. Setting an OCR as primary does not cause the related Opportunity record to update. If your email is being sent as a time dependent action on a workflow, you will want to ensure the Opportunity record is updated just prior to the email alert.
Solution 2:
If all of your OpportunityContactRoles are going to be created via the standard Opportunity layout, you can create a custom VisualForce page and controller to do this behind the scenes anytime you navigate to an Opportunity. The controller uses an @future method which queries the primary contact roles and updates the Opportunity lookup. This method is called via the action attribute on the VisualForce page. Add the VisualForce page to all Opportunity layouts as a hidden section (height and width set to 0, do not display label or section header). You can find code for this clever workaround here, but it won't apply to OpportunityContactRoles inserted via the API. These won't be updated until a user navigates to the record of the parent Opportunity.