This is a little rough and ready without any customisation - but might give you a rough idea.
- Created a custom table of type Activity called 'Private Message'
- Used the out-the-box activity fields e.g. description, regarding, created on.
- Created an Insert basic form called Private Message surfacing the description/regarding fields.
- Used Fetch XML to display the results for the current logged-in user where status = Open
The next stage would be to do the 'mark as read' aspect using the webapi when clicked, or insert a mark as read button per message. I would also create a new field for a contact lookup instead of using the regarding field, so this could be displayed in a dropdown via metadata.
{% fetchxml messages %}
<fetch>
<entity name="gw_privatemessage">
<attribute name="activityid" />
<attribute name="subject" />
<attribute name="createdon" />
<attribute name="description" />
<order attribute="subject" descending="false" />
<filter type="and">
<condition attribute="regardingobjectid" operator="eq" value="{{user.id}}" />
<condition attribute="createdon" operator="ne" value="{{now}}" />
<condition attribute="statecode" operator="eq" value="0" />
</filter>
</entity>
</fetch>
{% endfetchxml %}
<div class="message-item">
<h3>Send a new message</h3>
{% entityform name:'PrivateMessage' %}
</div>
<hr/>
{% if messages.results.entities.size > 0 %}
<h3>You have {{messages.results.entities.size}} unread messages</h3>
{% for result in messages.results.entities %}
<div class="message-item">{{ result.description }} <br/> <span>{{result.createdon}}</span></div>
{% endfor %}
{% else %}
<div class="alert alert-warning">You have no messages</div>
{% endif %}


Hope this helps you get started!