Don't worry, You're Doing It Right. Unit tests are almost always larger than the code they're testing, assuming you're doing actual unit tests, and not just smoke tests, because you need to check a lot of conditions in order to make sure everything is working correctly. Unit testing comes in three phases: setup, test, and validation. In the setup phase, you create any records you need for the unit test. Next, the test phase itself is calling the method or trigger to verify. Finally, the validation phase checks all the output for correct behavior.
A complete test will typically see the setup phase be of "medium" length, the test phase be of "short" length, and the validation phase be of "long" length. This is the nature of unit testing. You might choose to write some methods to validate data in a consistent manner across multiple unit tests, but ultimately, the validation phase will still require the most amount of effort (and is often the reason why many developers don't do it, despite repeated warnings and anecdotal evidence).
It's rare to see a validation phase that is too overkill in terms of validating business logic. My general rules for avoiding excess are simple: don't test system libraries or standard system logic, but do test everything else. For example, we don't write unit tests to verify that Math.PI is still 3.1415926..., because it will always be some close approximation. We don't
System.assert(2, 1+1), because such a failure can't happen (or, at least, if it did, there's nothing we can do about it...).
While it may seem your unit test is a bit overkill, you're on the right track. You're making sure that your for loops don't iterate over too many, or two few, items. You're making sure that the content of each email is correct. Basic testing of boundary conditions and outputs are common, and fundamental to appropriate unit testing.
If you're in a time crunch, you might choose to skip some of the checks, but keep in mind that the more you skip, the more you risk that a future developer could unintentionally break critical system functionality when maintaining the code or adding new features. This requires some risk analysis. The more you test, the longer it takes, but the less risk is involved.
As you become a more experienced developer, you might find ways to "imply" correct behavior. While I don't see any obvious time saves I'd do in your code (it really does look okay to me), I might do optimizations where a failure results in an exception, thus alerting me to the problem. For example:
SomeObject__c record = [SELECT Name FROM SomeObject__c];
As you may or may not know, the "scalar" assignment will fail if there is not exactly one record returned. Here, I don't assert that the result size was 1 before I checked the record. The system does this for me automatically by way of a standard exception. I've eliminated a line of code. (note: this is just a contrived example; I wouldn't normally do this in live code).
There's almost always ways to reduce the code you need to write if you leverage standard system features. I don't recommend this until you gain quite a bit of experience, and even then, you might still choose to be more explicit in order to help other, less experienced developers who might need to maintain your code later, but you should at least be aware that shortcuts typically do exist if you're careful. Always make sure you document them in your code, as well.