Repository: tayler6000/pyVoIP (language: python)

Issue:
Splitting SIPMessage object into SIPRequest and SIPResponse subclasses



Discussion / hints:

Fix patch:
diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index bb4f65c..946224f 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -11,11 +11,13 @@
     InvalidAccountInfoError,
     RetryRequiredError,
 )
-from pyVoIP.SIP.message import (
+from pyVoIP.SIP.message.message import (
     SIPMessage,
-    SIPStatus,
-    SIPMessageType,
+    SIPMethod,
+    SIPResponse,
+    SIPRequest,
 )
+from pyVoIP.SIP.message.response_codes import ResponseCode
 from pyVoIP.types import KEY_PASSWORD
 from pyVoIP.VoIP.status import PhoneStatus
 import pyVoIP
@@ -35,10 +37,14 @@
 
 
 UNAUTORIZED_RESPONSE_CODES = [
-    SIPStatus.UNAUTHORIZED,
-    SIPStatus.PROXY_AUTHENTICATION_REQUIRED,
+    ResponseCode.UNAUTHORIZED,
+    ResponseCode.PROXY_AUTHENTICATION_REQUIRED,
+]
+INVITE_OK_RESPONSE_CODES = [
+    ResponseCode.TRYING,
+    ResponseCode.RINGING,
+    ResponseCode.OK,
 ]
-INVITE_OK_RESPONSE_CODES = [SIPStatus.TRYING, SIPStatus.RINGING, SIPStatus.OK]
 
 
 class SIPClient:
@@ -107,7 +113,7 @@ def recv(self) -> None:
                 raw = self.s.recv(8192)
                 if raw != b"\x00\x00\x00\x00":
                     try:
-                        message = SIPMessage(raw)
+                        message = SIPMessage.from_bytes(raw)
                         debug(message.summary())
                         self.parse_message(message)
                     except Exception as ex:
@@ -133,13 +139,13 @@ def recv(self) -> None:
                         raise
 
     def handle_new_connection(self, conn: "VoIPConnection") -> None:
-        message = SIPMessage(conn.peak())
-        if message.type == SIPMessageType.REQUEST:
-            if message.method == "INVITE":
+        message = SIPMessage.from_bytes(conn.peak())
+        if type(message) is SIPRequest:
+            if message.method == SIPMethod.INVITE:
                 self._handle_invite(conn)
 
     def _handle_invite(self, conn: "VoIPConnection") -> None:
-        message = SIPMessage(conn.peak())
+        message = SIPMessage.from_bytes(conn.peak())
         if self.call_callback is None:
             request = self.gen_busy(message)
             conn.send(request)
@@ -147,20 +153,20 @@ def _handle_invite(self, conn: "VoIPConnection") -> None:
             self.call_callback(conn, message)
 
     def parse_message(self, message: SIPMessage) -> None:
-        if message.type != SIPMessageType.REQUEST:
+        if type(message) is SIPResponse:
             if message.status in (
-                SIPStatus.OK,
-                SIPStatus.NOT_FOUND,
-                SIPStatus.SERVICE_UNAVAILABLE,
-                SIPStatus.PROXY_AUTHENTICATION_REQUIRED,
-                SIPStatus.RINGING,
-                SIPStatus.BUSY_HERE,
-                SIPStatus.SESSION_PROGRESS,
-                SIPStatus.REQUEST_TERMINATED,
+                ResponseCode.OK,
+                ResponseCode.NOT_FOUND,
+                ResponseCode.SERVICE_UNAVAILABLE,
+                ResponseCode.PROXY_AUTHENTICATION_REQUIRED,
+                ResponseCode.RINGING,
+                ResponseCode.BUSY_HERE,
+                ResponseCode.SESSION_PROGRESS,
+                ResponseCode.REQUEST_TERMINATED,
             ):
                 if self.call_callback is not None:
                     self.call_callback(message)
-            elif message.status == SIPStatus.TRYING:
+            elif message.status == ResponseCode.TRYING:
                 pass
             else:
                 debug(
@@ -169,38 +175,39 @@ def parse_message(self, message: SIPMessage) -> None:
                     "TODO: Add 500 Error on Receiving SIP Response",
                 )
             return
-        elif message.method == "BYE":
-            # TODO: If callCallback is None, the call doesn't exist, 481
-            if self.call_callback:
-                self.call_callback(message)
-            response = self.gen_ok(message)
-            try:
-                # BYE comes from client cause server only acts as mediator
-                (_sender_adress, _sender_port) = message.headers["Via"][0][
-                    "address"
-                ]
-                self.sendto(
-                    response,
-                    (_sender_adress, int(_sender_port)),
-                )
-            except Exception:
-                debug("BYE Answer failed falling back to server as target")
+        elif type(message) is SIPRequest:
+            if message.method == "BYE":
+                # TODO: If callCallback is None, the call doesn't exist, 481
+                if self.call_callback:
+                    self.call_callback(message)
+                response = self.gen_ok(message)
+                try:
+                    # BYE comes from client cause server only acts as mediator
+                    (_sender_adress, _sender_port) = message.headers["Via"][0][
+                        "address"
+                    ]
+                    self.sendto(
+                        response,
+                        (_sender_adress, int(_sender_port)),
+                    )
+                except Exception:
+                    debug("BYE Answer failed falling back to server as target")
+                    self.sendto(response, message.headers["Via"]["address"])
+            elif message.method == "ACK":
+                return
+            elif message.method == "CANCEL":
+                # TODO: If callCallback is None, the call doesn't exist, 481
+                self.call_callback(message)  # type: ignore
+                response = self.gen_ok(message)
+                self.sendto(response, message.headers["Via"]["address"])
+            elif message.method == "OPTIONS":
+                if self.call_callback:
+                    response = str(self.call_callback(message))
+                else:
+                    response = self._gen_options_response(message)
                 self.sendto(response, message.headers["Via"]["address"])
-        elif message.method == "ACK":
-            return
-        elif message.method == "CANCEL":
-            # TODO: If callCallback is None, the call doesn't exist, 481
-            self.call_callback(message)  # type: ignore
-            response = self.gen_ok(message)
-            self.sendto(response, message.headers["Via"]["address"])
-        elif message.method == "OPTIONS":
-            if self.call_callback:
-                response = str(self.call_callback(message))
             else:
-                response = self._gen_options_response(message)
-            self.sendto(response, message.headers["Via"]["address"])
-        else:
-            debug("TODO: Add 400 Error on non processable request")
+                debug("TODO: Add 400 Error on non processable request")
 
     def start(self) -> None:
         if self.NSD:
@@ -1109,25 +1116,32 @@ def invite(
         )
         conn = self.sendto(invite)
         debug("Invited")
-        response = SIPMessage(conn.recv(8192))
+        response = SIPMessage.from_bytes(conn.recv(8192))
 
         while (
-            response.status
-            not in UNAUTORIZED_RESPONSE_CODES + INVITE_OK_RESPONSE_CODES
-        ) or response.headers["Call-ID"] != call_id:
+            type(response) is SIPResponse
+            and (
+                response.status
+                not in UNAUTORIZED_RESPONSE_CODES + INVITE_OK_RESPONSE_CODES
+            )
+            or response.headers["Call-ID"] != call_id
+        ):
             if not self.NSD:
                 break
             debug(f"Received Response: {response.summary()}")
             self.parse_message(response)
-            response = SIPMessage(conn.recv(8192))
+            response = SIPMessage.from_bytes(conn.recv(8192))
 
         debug(f"Received Response: {response.summary()}")
 
-        if response.status in INVITE_OK_RESPONSE_CODES:
+        if (
+            type(response) is SIPResponse
+            and response.status in INVITE_OK_RESPONSE_CODES
+        ):
             debug("Invite Accepted")
-            if response.status is SIPStatus.OK:
+            if response.status is ResponseCode.OK:
                 return response, call_id, sess_id, conn
-            return SIPMessage(invite.encode("utf8")), call_id, sess_id, conn
+            return SIPMessage.from_string(invite), call_id, sess_id, conn
         debug("Invite Requires Authorization")
         ack = self.gen_ack(response)
         conn.send(ack)
@@ -1144,7 +1158,7 @@ def invite(
 
         conn = self.sendto(invite)
 
-        return SIPMessage(invite.encode("utf8")), call_id, sess_id, conn
+        return SIPMessage.from_string(invite), call_id, sess_id, conn
 
     def gen_message(
         self, number: str, body: str, ctype: str, branch: str, call_id: str
@@ -1186,14 +1200,16 @@ def message(
         debug("Message")
         auth = False
         while True:
-            response = SIPMessage(conn.recv(8192))
+            response = SIPMessage.from_bytes(conn.recv(8192))
             debug(f"Received Response: {response.summary()}")
             self.parse_message(response)
-            if response.status == SIPStatus(100):
+            if type(response) is not SIPResponse:
+                continue
+            if response.status == ResponseCode(100):
                 continue
-            if response.status == SIPStatus(
+            if response.status == ResponseCode(
                 401
-            ) or response.status == SIPStatus(407):
+            ) or response.status == ResponseCode(407):
                 if auth:
                     debug("Auth Failure")
                     break
@@ -1204,7 +1220,7 @@ def message(
                 )
                 conn.send(msg)
                 continue
-            if response.status == SIPStatus.OK:
+            if response.status == ResponseCode.OK:
                 break
             if self.NSD:
                 break
@@ -1220,8 +1236,8 @@ def bye(self, request: SIPMessage) -> None:
                 request.headers["Contact"]["port"],
             ),
         )
-        response = SIPMessage(conn.recv(8192))
-        if response.status == SIPStatus(401):
+        response = SIPMessage.from_bytes(conn.recv(8192))
+        if response.status == ResponseCode(401):
             #  Requires password
             auth = self.gen_authorization(response)
             message = message.replace(
@@ -1267,35 +1283,35 @@ def __deregister(self) -> bool:
         first_response = response
         conn.close()  # Regardless of the response, the dialog is over
 
-        if response.status == SIPStatus(401):
+        if response.status == ResponseCode(401):
             # Unauthorized, likely due to being password protected.
             password_request = self.gen_register(response, deregister=True)
             conn = self.send(password_request)
             response = self.__receive(conn)
             conn.close()
 
-        if response.status == SIPStatus(400):
+        if response.status == ResponseCode(400):
             # Bad Request
             # TODO: implement
             # TODO: check if broken connection can be brought back
             # with new urn:uuid or reply with expire 0
             self._handle_bad_request()
 
-        elif response.status == SIPStatus(407):
+        elif response.status == ResponseCode(407):
             # Proxy Authentication Required
             # TODO: implement
             debug("Proxy auth required")
 
-        elif response.status == SIPStatus(500):
+        elif response.status == ResponseCode(500):
             # We raise so the calling function can sleep and try again
             raise RetryRequiredError(
                 "Received a 500 error when deregistering."
             )
 
-        elif response.status == SIPStatus.OK:
+        elif response.status == ResponseCode.OK:
             return True
 
-        elif response.status == SIPStatus(401):
+        elif response.status == ResponseCode(401):
             # At this point, it's reasonable to assume that
             # this is caused by invalid credentials.
             debug("=" * 50)
@@ -1368,33 +1384,33 @@ def __register(self) -> bool:
         first_response = response
         conn.close()  # Regardless of the response, the dialog is over
 
-        if response.status == SIPStatus(401):
+        if response.status == ResponseCode(401):
             # Unauthorized, likely due to being password protected.
             password_request = self.gen_register(response)
             conn = self.send(password_request)
             response = self.__receive(conn)
             conn.close()
 
-        if response.status == SIPStatus(400):
+        if response.status == ResponseCode(400):
             # Bad Request
             # TODO: implement
             # TODO: check if broken connection can be brought back
             # with new urn:uuid or reply with expire 0
             self._handle_bad_request()
 
-        elif response.status == SIPStatus(407):
+        elif response.status == ResponseCode(407):
             # Proxy Authentication Required
             # TODO: implement
             debug("Proxy auth required")
 
-        elif response.status == SIPStatus(500):
+        elif response.status == ResponseCode(500):
             # We raise so the calling function can sleep and try again
             raise RetryRequiredError("Received a 500 error when registering.")
 
-        elif response.status == SIPStatus.OK:
+        elif response.status == ResponseCode.OK:
             return True
 
-        elif response.status == SIPStatus(401):
+        elif response.status == ResponseCode(401):
             # At this point, it's reasonable to assume that
             # this is caused by invalid credentials.
             debug("=" * 50)
@@ -1428,25 +1444,34 @@ def subscribe(self, lastresponse: SIPMessage) -> None:
         subRequest = self.gen_subscribe(lastresponse)
         conn = self.sendto(subRequest)
 
-        response = SIPMessage(conn.recv(8192))
+        response = SIPMessage.from_bytes(conn.recv(8192))
 
-        debug(f'Got response to subscribe: {str(response.heading, "utf8")}')
+        debug(f'Got response to subscribe: {str(response.start_line, "utf8")}')
 
-    def __receive(self, conn: "VoIPConnection") -> SIPMessage:
+    def __receive(self, conn: "VoIPConnection") -> SIPResponse:
         """
         Some servers need time to process the response.
         When this happens, the first response you get from the server is
-        SIPStatus.TRYING. This while loop tries checks every second for an
+        ResponseCode.TRYING. This while loop tries checks every second for an
         updated response. It times out after 30 seconds with no response.
         """
         try:
-            response = SIPMessage(conn.recv(8128, self.register_timeout))
-            while response.status == SIPStatus.TRYING and self.NSD:
-                response = SIPMessage(conn.recv(8128, self.register_timeout))
+            response = SIPMessage.from_bytes(
+                conn.recv(8128, self.register_timeout)
+            )
+            while (
+                type(response) is SIPResponse
+                and response.status == ResponseCode.TRYING
+                and self.NSD
+            ):
+                response = SIPMessage.from_bytes(
+                    conn.recv(8128, self.register_timeout)
+                )
                 time.sleep(1)
         except TimeoutError:
             raise TimeoutError(
                 f"Waited {self.register_timeout} seconds but the server is "
                 + "still TRYING or has not responded."
             )
+        assert type(response) is SIPResponse
         return response
diff --git a/pyVoIP/SIP/message.py b/pyVoIP/SIP/message.py
deleted file mode 100644
index 84b4dc7..0000000
--- a/pyVoIP/SIP/message.py
+++ /dev/null
@@ -1,829 +0,0 @@
-from enum import Enum, IntEnum
-from pyVoIP import regex
-from pyVoIP.SIP.error import SIPParseError
-from pyVoIP.types import URI_HEADER
-from typing import Any, Callable, Dict, List, Optional, Union
-import pyVoIP
-
-
-debug = pyVoIP.debug
-
-
-class SIPStatus(Enum):
-    def __new__(cls, value: int, phrase: str = "", description: str = ""):
-        obj = object.__new__(cls)
-        obj._value_ = value
-
-        obj.phrase = phrase
-        obj.description = description
-        return obj
-
-    def __int__(self) -> int:
-        return self._value_
-
-    def __str__(self) -> str:
-        return f"{self._value_} {self.phrase}"
-
-    @property
-    def phrase(self) -> str:
-        return self._phrase
-
-    @phrase.setter
-    def phrase(self, value: str) -> None:
-        self._phrase = value
-
-    @property
-    def description(self) -> str:
-        return self._description
-
-    @description.setter
-    def description(self, value: str) -> None:
-        self._description = value
-
-    # Informational
-    TRYING = (
-        100,
-        "Trying",
-        "Extended search being performed, may take a significant time",
-    )
-    RINGING = (
-        180,
-        "Ringing",
-        "Destination user agent received INVITE, "
-        + "and is alerting user of call",
-    )
-    FORWARDED = 181, "Call is Being Forwarded"
-    QUEUED = 182, "Queued"
-    SESSION_PROGRESS = 183, "Session Progress"
-    TERMINATED = 199, "Early Dialog Terminated"
-
-    # Success
-    OK = 200, "OK", "Request successful"
-    ACCEPTED = (
-        202,
-        "Accepted",
-        "Request accepted, processing continues (Deprecated.)",
-    )
-    NO_NOTIFICATION = (
-        204,
-        "No Notification",
-        "Request fulfilled, nothing follows",
-    )
-
-    # Redirection
-    MULTIPLE_CHOICES = (
-        300,
-        "Multiple Choices",
-        "Object has several resources -- see URI list",
-    )
-    MOVED_PERMANENTLY = (
-        301,
-        "Moved Permanently",
-        "Object moved permanently -- see URI list",
-    )
-    MOVED_TEMPORARILY = (
-        302,
-        "Moved Temporarily",
-        "Object moved temporarily -- see URI list",
-    )
-    USE_PROXY = (
-        305,
-        "Use Proxy",
-        "You must use proxy specified in Location to "
-        + "access this resource",
-    )
-    ALTERNATE_SERVICE = (
-        380,
-        "Alternate Service",
-        "The call failed, but alternatives are available -- see URI list",
-    )
-
-    # Client Error
-    BAD_REQUEST = (
-        400,
-        "Bad Request",
-        "Bad request syntax or unsupported method",
-    )
-    UNAUTHORIZED = (
-        401,
-        "Unauthorized",
-        "No permission -- see authorization schemes",
-    )
-    PAYMENT_REQUIRED = (
-        402,
-        "Payment Required",
-        "No payment -- see charging schemes",
-    )
-    FORBIDDEN = (
-        403,
-        "Forbidden",
-        "Request forbidden -- authorization will not help",
-    )
-    NOT_FOUND = (404, "Not Found", "Nothing matches the given URI")
-    METHOD_NOT_ALLOWED = (
-        405,
-        "Method Not Allowed",
-        "Specified method is invalid for this resource",
-    )
-    NOT_ACCEPTABLE = (
-        406,
-        "Not Acceptable",
-        "URI not available in preferred format",
-    )
-    PROXY_AUTHENTICATION_REQUIRED = (
-        407,
-        "Proxy Authentication Required",
-        "You must authenticate with this proxy before proceeding",
-    )
-    REQUEST_TIMEOUT = (
-        408,
-        "Request Timeout",
-        "Request timed out; try again later",
-    )
-    CONFLICT = 409, "Conflict", "Request conflict"
-    GONE = (
-        410,
-        "Gone",
-        "URI no longer exists and has been permanently removed",
-    )
-    LENGTH_REQUIRED = (
-        411,
-        "Length Required",
-        "Client must specify Content-Length",
-    )
-    CONDITIONAL_REQUEST_FAILED = 412, "Conditional Request Failed"
-    REQUEST_ENTITY_TOO_LARGE = (
-        413,
-        "Request Entity Too Large",
-        "Entity is too large",
-    )
-    REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long", "URI is too long"
-    UNSUPPORTED_MEDIA_TYPE = (
-        415,
-        "Unsupported Media Type",
-        "Entity body in unsupported format",
-    )
-    UNSUPPORTED_URI_SCHEME = (
-        416,
-        "Unsupported URI Scheme",
-        "Cannot satisfy request",
-    )
-    UNKOWN_RESOURCE_PRIORITY = (
-        417,
-        "Unkown Resource-Priority",
-        "There was a resource-priority option tag, "
-        + "but no Resource-Priority header",
-    )
-    BAD_EXTENSION = (
-        420,
-        "Bad Extension",
-        "Bad SIP Protocol Extension used, not understood by the server.",
-    )
-    EXTENSION_REQUIRED = (
-        421,
-        "Extension Required",
-        "Server requeires a specific extension to be "
-        + "listed in the Supported header.",
-    )
-    SESSION_INTERVAL_TOO_SMALL = 422, "Session Interval Too Small"
-    SESSION_INTERVAL_TOO_BRIEF = 423, "Session Interval Too Breif"
-    BAD_LOCATION_INFORMATION = 424, "Bad Location Information"
-    USE_IDENTITY_HEADER = (
-        428,
-        "Use Identity Header",
-        "The server requires an Identity header, "
-        + "and one has not been provided.",
-    )
-    PROVIDE_REFERRER_IDENTITY = 429, "Provide Referrer Identity"
-    """
-    This response is intended for use between proxy devices,
-    and should not be seen by an endpoint. If it is seen by one,
-    it should be treated as a 400 Bad Request response.
-    """
-    FLOW_FAILED = (
-        430,
-        "Flow Failed",
-        "A specific flow to a user agent has failed, "
-        + "although other flows may succeed.",
-    )
-    ANONYMITY_DISALLOWED = 433, "Anonymity Disallowed"
-    BAD_IDENTITY_INFO = 436, "Bad Identity-Info"
-    UNSUPPORTED_CERTIFICATE = 437, "Unsupported Certificate"
-    INVALID_IDENTITY_HEADER = 438, "Invalid Identity Header"
-    FIRST_HOP_LACKS_OUTBOUND_SUPPORT = 439, "First Hop Lacks Outbound Support"
-    MAX_BREADTH_EXCEEDED = 440, "Max-Breadth Exceeded"
-    BAD_INFO_PACKAGE = 469, "Bad Info Package"
-    CONSENT_NEEDED = 470, "Consent Needed"
-    TEMPORARILY_UNAVAILABLE = 480, "Temporarily Unavailable"
-    CALL_OR_TRANSACTION_DOESNT_EXIST = 481, "Call/Transaction Does Not Exist"
-    LOOP_DETECTED = 482, "Loop Detected"
-    TOO_MANY_HOPS = 483, "Too Many Hops"
-    ADDRESS_INCOMPLETE = 484, "Address Incomplete"
-    AMBIGUOUS = 485, "Ambiguous"
-    BUSY_HERE = 486, "Busy Here", "Callee is busy"
-    REQUEST_TERMINATED = 487, "Request Terminated"
-    NOT_ACCEPTABLE_HERE = 488, "Not Acceptable Here"
-    BAD_EVENT = 489, "Bad Event"
-    REQUEST_PENDING = 491, "Request Pending"
-    UNDECIPHERABLE = 493, "Undecipherable"
-    SECURITY_AGREEMENT_REQUIRED = 494, "Security Agreement Required"
-
-    # Server Errors
-    INTERNAL_SERVER_ERROR = (
-        500,
-        "Internal Server Error",
-        "Server got itself in trouble",
-    )
-    NOT_IMPLEMENTED = (
-        501,
-        "Not Implemented",
-        "Server does not support this operation",
-    )
-    BAD_GATEWAY = (
-        502,
-        "Bad Gateway",
-        "Invalid responses from another server/proxy",
-    )
-    SERVICE_UNAVAILABLE = (
-        503,
-        "Service Unavailable",
-        "The server cannot process the request due to a high load",
-    )
-    GATEWAY_TIMEOUT = (
-        504,
-        "Server Timeout",
-        "The server did not receive a timely response",
-    )
-    SIP_VERSION_NOT_SUPPORTED = (
-        505,
-        "SIP Version Not Supported",
-        "Cannot fulfill request",
-    )
-    MESSAGE_TOO_LONG = 513, "Message Too Long"
-    PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = (
-        555,
-        "Push Notification Service Not Supported",
-    )
-    PRECONDITION_FAILURE = 580, "Precondition Failure"
-
-    # Global Failure Responses
-    BUSY_EVERYWHERE = 600, "Busy Everywhere"
-    DECLINE = 603, "Decline"
-    DOES_NOT_EXIST_ANYWHERE = 604, "Does Not Exist Anywhere"
-    GLOBAL_NOT_ACCEPTABLE = 606, "Not Acceptable"
-    UNWANTED = 607, "Unwanted"
-    REJECTED = 608, "Rejected"
-
-
-class SIPMessageType(IntEnum):
-    def __new__(cls, value: int):
-        obj = int.__new__(cls, value)
-        obj._value_ = value
-        return obj
-
-    REQUEST = 1
-    RESPONSE = 0
-
-
-class SIPMessage:
-    def __init__(self, data: bytes):
-        self.SIPCompatibleVersions = pyVoIP.SIPCompatibleVersions
-        self.SIPCompatibleMethods = pyVoIP.SIPCompatibleMethods
-        self.heading: List[str] = []
-        self.type: Optional[SIPMessageType] = None
-        self.status = SIPStatus(491)
-        self.headers: Dict[str, Any] = {"Via": []}
-        self.body: Dict[str, Any] = {}
-        self.authentication: Dict[str, Union[str, List[str]]] = {}
-        self.raw = data
-        self.auth_match = regex.AUTH_MATCH
-
-        # Compacts defined in RFC 3261 Section 7.3.3 and 20
-        self.compact_key = {
-            "i": "Call-ID",
-            "m": "Contact",
-            "e": "Content-Encoding",
-            "l": "Content-Length",
-            "c": "Content-Type",
-            "f": "From",
-            "s": "Subject",
-            "k": "Supported",
-            "t": "To",
-            "v": "Via",
-        }
-
-        try:
-            self.parse(data)
-        except Exception as e:
-            if type(e) is not SIPParseError:
-                raise SIPParseError(e) from e
-            raise
-
-    @property
-    def to(self) -> Optional[URI_HEADER]:
-        """
-        The to property specifies the URI in the first line of a SIP request.
-        """
-        return self._to
-
-    @to.setter
-    def to(self, value: str) -> None:
-        self._to = value
-
-    def summary(self) -> str:
-        data = ""
-        data += f"{' '.join(self.heading)}\n\n"
-        data += "Headers:\n"
-        for x in self.headers:
-            data += f"{x}: {self.headers[x]}\n"
-        data += "\n"
-        data += "Body:\n"
-        for x in self.body:
-            data += f"{x}: {self.body[x]}\n"
-        data += "\n"
-        data += "Raw:\n"
-        data += str(self.raw)
-
-        return data
-
-    def parse(self, data: bytes) -> None:
-        try:
-            headers, body = data.split(b"\r\n\r\n")
-        except ValueError as ve:
-            debug(f"Error unpacking data, only using header: {ve}")
-            headers = data.split(b"\r\n\r\n")[0]
-            body = b""
-
-        headers_raw = headers.split(b"\r\n")
-        self.heading = str(headers_raw.pop(0), "utf8").split(" ")
-        check = self.heading[0]
-        data = b"\r\n".join(headers_raw) + b"\r\n\r\n" + body
-
-        if check in self.SIPCompatibleVersions:
-            self.type = SIPMessageType.RESPONSE
-            self.parse_sip_response(data)
-        else:  # elif check in self.SIPCompatibleMethods:
-            self.type = SIPMessageType.REQUEST
-            self.parse_sip_request(data)
-        """
-        else:
-            raise SIPParseError(
-                "Unable to decipher SIP request: " + str(heading, "utf8")
-            )
-        """
-
-    def __get_uri_header(self, data: str) -> URI_HEADER:
-        info = data.split(";tag=")
-        tag = ""
-        if len(info) >= 2:
-            tag = info[1]
-        raw = data
-        reg = regex.TO_FROM_MATCH
-        direct = "@" not in data
-        if direct:
-            reg = regex.TO_FROM_DIRECT_MATCH
-        match = reg.match(data)
-        if match is None:
-            raise SIPParseError(
-                "Regex failed to match To/From.\n\n"
-                + "Please open a GitHub Issue at "
-                + "https://www.github.com/tayler6000/pyVoIP "
-                + "and include the following:\n\n"
-                + f"{data=} {type(match)=}"
-            )
-        matches = match.groupdict()
-        if direct:
-            matches["user"] = ""
-            matches["password"] = ""
-        uri = f'{matches["uri_type"]}:{matches["user"]}@{matches["host"]}'
-        if direct:
-            uri = f'{matches["uri_type"]}:{matches["host"]}'
-        if matches["port"]:
-            uri += matches["port"]
-        uri_type = matches["uri_type"]
-        user = matches["user"]
-        password = (
-            matches["password"].strip(":") if matches["password"] else ""
-        )
-        display_name = (
-            matches["display_name"].strip().strip('"')
-            if matches["display_name"]
-            else ""
-        )
-        host = matches["host"]
-        port = int(matches["port"].strip(":")) if matches["port"] else 5060
-
-        return {
-            "raw": raw,
-            "tag": tag,
-            "uri": uri,
-            "uri-type": uri_type,
-            "user": user,
-            "password": password,
-            "display-name": display_name,
-            "host": host,
-            "port": port,
-        }
-
-    def parse_header(self, header: str, data: str) -> None:
-        if header in self.compact_key.keys():
-            header = self.compact_key[header]
-
-        if header == "Via":
-            for d in data:
-                info = regex.VIA_SPLIT.split(d)
-                _type = info[0]  # SIP Method
-                _address = info[1].split(":")  # Tuple: address, port
-                _ip = _address[0]
-
-                """
-                If no port is provided in via header assume default port.
-                Needs to be str. Check response build for better str creation
-                """
-                _port = (
-                    int(info[1].split(":")[1]) if len(_address) > 1 else 5060
-                )
-                _via = {"type": _type, "address": (_ip, _port)}
-
-                """
-                Sets branch, maddr, ttl, received, and rport if defined
-                as per RFC 3261 20.7
-                """
-                for x in info[2:]:
-                    if "=" in x:
-                        try:
-                            _via[x.split("=")[0]] = int(x.split("=")[1])
-                        except ValueError:
-                            _via[x.split("=")[0]] = x.split("=")[1]
-                    else:
-                        _via[x] = None
-                self.headers["Via"].append(_via)
-        elif header in ["To", "From", "Contact", "Refer-To"]:
-            self.headers[header] = self.__get_uri_header(data)
-        elif header == "CSeq":
-            self.headers[header] = {
-                "check": int(data.split(" ")[0]),
-                "method": data.split(" ")[1],
-            }
-        elif header in ["Allow", "Supported", "Require"]:
-            self.headers[header] = data.split(", ")
-        elif header == "Call-ID":
-            self.headers[header] = data
-        elif header in (
-            "WWW-Authenticate",
-            "Authorization",
-            "Proxy-Authenticate",
-        ):
-            method = data.split(" ")[0]
-            data = data.replace(f"{method} ", "")
-            row_data = self.auth_match.findall(data)
-            header_data: Dict[str, Any] = {"header": header, "method": method}
-            for var, data in row_data:
-                if var == "userhash":
-                    header_data[var] = (
-                        False if data.strip('"').lower() == "false" else True
-                    )
-                    continue
-                if var == "qop":
-                    authorized = data.strip('"').split(",")
-                    for i, value in enumerate(authorized):
-                        authorized[i] = value.strip()
-                    header_data[var] = authorized
-                    continue
-                header_data[var] = data.strip('"')
-            self.headers[header] = header_data
-            self.authentication = header_data
-        elif header == "Target-Dialog":
-            # Target-Dialog (tdialog) is specified in RFC 4538
-            params = data.split(";")
-            header_data: Dict[str, Any] = {
-                "callid": params.pop(0)
-            }  # key is callid to be consitenent with RFC 4538 Section 7
-            for x in params:
-                y = x.split("=")
-                header_data[y[0]] = y[1]
-            self.headers[header] = header_data
-        elif header == "Refer-Sub":
-            # Refer-Sub (norefersub) is specified in RFC 4488
-            params = data.split(";")
-            header_data: Dict[str, Any] = {
-                "value": True if params.pop(0) == "true" else False
-            }  # BNF states extens are possible
-            for x in params:
-                y = x.split("=")
-                header_data[y[0]] = y[1]
-            self.headers[header] = header_data
-        else:
-            try:
-                self.headers[header] = int(data)
-            except ValueError:
-                self.headers[header] = data
-
-    def parse_body(self, header: str, data: str) -> None:
-        if "Content-Encoding" in self.headers:
-            raise SIPParseError("Unable to parse encoded content.")
-        if self.headers["Content-Type"] == "application/sdp":
-            # Referenced RFC 4566 July 2006
-            if header == "v":
-                # SDP 5.1 Version
-                self.body[header] = int(data)
-            elif header == "o":
-                # SDP 5.2 Origin
-                # o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address> # noqa: E501
-                d = data.split(" ")
-                self.body[header] = {
-                    "username": d[0],
-                    "id": d[1],
-                    "version": d[2],
-                    "network_type": d[3],
-                    "address_type": d[4],
-                    "address": d[5],
-                }
-            elif header == "s":
-                # SDP 5.3 Session Name
-                # s=<session name>
-                self.body[header] = data
-            elif header == "i":
-                # SDP 5.4 Session Information
-                # i=<session-description>
-                self.body[header] = data
-            elif header == "u":
-                # SDP 5.5 URI
-                # u=<uri>
-                self.body[header] = data
-            elif header == "e" or header == "p":
-                # SDP 5.6 Email Address and Phone Number of person
-                # responsible for the conference
-                # e=<email-address>
-                # p=<phone-number>
-                self.body[header] = data
-            elif header == "c":
-                # SDP 5.7 Connection Data
-                # c=<nettype> <addrtype> <connection-address>
-                if "c" not in self.body:
-                    self.body["c"] = []
-                d = data.split(" ")
-                # TTL Data and Multicast addresses may be specified.
-                # For IPv4 its listed as addr/ttl/number of addresses.
-                # c=IN IP4 224.2.1.1/127/3 means:
-                # c=IN IP4 224.2.1.1/127
-                # c=IN IP4 224.2.1.2/127
-                # c=IN IP4 224.2.1.3/127
-                # With the TTL being 127.
-                # IPv6 does not support time to live so you will only see a '/'
-                # for multicast addresses.
-                if "/" in d[2]:
-                    if d[1] == "IP6":
-                        self.body[header].append(
-                            {
-                                "network_type": d[0],
-                                "address_type": d[1],
-                                "address": d[2].split("/")[0],
-                                "ttl": None,
-                                "address_count": int(d[2].split("/")[1]),
-                            }
-                        )
-                    else:
-                        address_data = d[2].split("/")
-                        if len(address_data) == 2:
-                            self.body[header].append(
-                                {
-                                    "network_type": d[0],
-                                    "address_type": d[1],
-                                    "address": address_data[0],
-                                    "ttl": int(address_data[1]),
-                                    "address_count": 1,
-                                }
-                            )
-                        else:
-                            self.body[header].append(
-                                {
-                                    "network_type": d[0],
-                                    "address_type": d[1],
-                                    "address": address_data[0],
-                                    "ttl": int(address_data[1]),
-                                    "address_count": int(address_data[2]),
-                                }
-                            )
-                else:
-                    self.body[header].append(
-                        {
-                            "network_type": d[0],
-                            "address_type": d[1],
-                            "address": d[2],
-                            "ttl": None,
-                            "address_count": 1,
-                        }
-                    )
-            elif header == "b":
-                # SDP 5.8 Bandwidth
-                # b=<bwtype>:<bandwidth>
-                # A bwtype of CT means Conference Total between all medias
-                # and all devices in the conference.
-                # A bwtype of AS means Applicaton Specific total for this
-                # media and this device.
-                # The bandwidth is given in kilobits per second.
-                # As this was written in 2006, this could be Kibibits.
-                # TODO: Implement Bandwidth restrictions
-                d = data.split(":")
-                self.body[header] = {"type": d[0], "bandwidth": d[1]}
-            elif header == "t":
-                # SDP 5.9 Timing
-                # t=<start-time> <stop-time>
-                d = data.split(" ")
-                self.body[header] = {"start": d[0], "stop": d[1]}
-            elif header == "r":
-                # SDP 5.10 Repeat Times
-                # r=<repeat interval> <active duration> <offsets from start-time> # noqa: E501
-                d = data.split(" ")
-                self.body[header] = {
-                    "repeat": d[0],
-                    "duration": d[1],
-                    "offset1": d[2],
-                    "offset2": d[3],
-                }
-            elif header == "z":
-                # SDP 5.11 Time Zones
-                # z=<adjustment time> <offset> <adjustment time> <offset> ....
-                # Used for change in timezones such as day light savings time.
-                d = data.split()
-                amount = len(d) / 2
-                self.body[header] = {}
-                for x in range(int(amount)):
-                    self.body[header]["adjustment-time" + str(x)] = d[x * 2]
-                    self.body[header]["offset" + str(x)] = d[x * 2 + 1]
-            elif header == "k":
-                # SDP 5.12 Encryption Keys
-                # k=<method>
-                # k=<method>:<encryption key>
-                if ":" in data:
-                    d = data.split(":")
-                    self.body[header] = {"method": d[0], "key": d[1]}
-                else:
-                    self.body[header] = {"method": d}
-            elif header == "m":
-                # SDP 5.14 Media Descriptions
-                # m=<media> <port>/<number of ports> <proto> <fmt> ...
-                # <port> should be even, and <port>+1 should be the RTCP port.
-                # <number of ports> should coinside with number of
-                # addresses in SDP 5.7 c=
-                if "m" not in self.body:
-                    self.body["m"] = []
-                d = data.split(" ")
-
-                if "/" in d[1]:
-                    ports_raw = d[1].split("/")
-                    port = ports_raw[0]
-                    count = int(ports_raw[1])
-                else:
-                    port = d[1]
-                    count = 1
-                methods = d[3:]
-
-                self.body["m"].append(
-                    {
-                        "type": d[0],
-                        "port": int(port),
-                        "port_count": count,
-                        "protocol": pyVoIP.RTP.RTPProtocol(d[2]),
-                        "methods": methods,
-                        "attributes": {},
-                    }
-                )
-                for x in self.body["m"][-1]["methods"]:
-                    self.body["m"][-1]["attributes"][x] = {}
-            elif header == "a":
-                # SDP 5.13 Attributes & 6.0 SDP Attributes
-                # a=<attribute>
-                # a=<attribute>:<value>
-
-                if "a" not in self.body:
-                    self.body["a"] = {}
-
-                if ":" in data:
-                    d = data.split(":")
-                    attribute = d[0]
-                    value = d[1]
-                else:
-                    attribute = data
-                    value = None
-
-                if value is not None:
-                    if attribute == "rtpmap":
-                        # a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] # noqa: E501
-                        v = regex.SDP_A_SPLIT.split(value)
-                        for t in self.body["m"]:
-                            if v[0] in t["methods"]:
-                                index = int(self.body["m"].index(t))
-                                break
-                        if len(v) == 4:
-                            encoding = v[3]
-                        else:
-                            encoding = None
-
-                        self.body["m"][index]["attributes"][v[0]]["rtpmap"] = {
-                            "id": v[0],
-                            "name": v[1],
-                            "frequency": v[2],
-                            "encoding": encoding,
-                        }
-
-                    elif attribute == "fmtp":
-                        # a=fmtp:<format> <format specific parameters>
-                        d = value.split(" ")
-                        for t in self.body["m"]:
-                            if d[0] in t["methods"]:
-                                index = int(self.body["m"].index(t))
-                                break
-
-                        self.body["m"][index]["attributes"][d[0]]["fmtp"] = {
-                            "id": d[0],
-                            "settings": d[1:],
-                        }
-                    else:
-                        self.body["a"][attribute] = value
-                else:
-                    if (
-                        attribute == "recvonly"
-                        or attribute == "sendrecv"
-                        or attribute == "sendonly"
-                        or attribute == "inactive"
-                    ):
-                        self.body["a"]["transmit_type"] = (
-                            pyVoIP.RTP.TransmitType(attribute)
-                        )  # noqa: E501
-            else:
-                self.body[header] = data
-
-        else:
-            self.body["content"] = data
-
-    @staticmethod
-    def parse_raw_header(
-        headers_raw: List[bytes], handle: Callable[[str, str], None]
-    ) -> None:
-        headers: Dict[str, Any] = {"Via": []}
-        # Only use first occurance of VIA header field;
-        # got second VIA from Kamailio running in DOCKER
-        # According to RFC 3261 these messages should be
-        # discarded in a response
-        for x in headers_raw:
-            i = str(x, "utf8").split(": ")
-            if i[0] == "Via":
-                headers["Via"].append(i[1])
-            if i[0] not in headers.keys():
-                headers[i[0]] = i[1]
-
-        for key, val in headers.items():
-            handle(key, val)
-
-    @staticmethod
-    def parse_raw_body(
-        body: bytes, ctype: str, handle: Callable[[str, str], None]
-    ) -> None:
-        if len(body) > 0:
-            if ctype == "application/sdp":
-                body_raw = body.split(b"\r\n")
-                for x in body_raw:
-                    i = str(x, "utf8").split("=")
-                    if i != [""]:
-                        handle(i[0], i[1])
-            else:
-                handle("", body)
-
-    def parse_sip_response(self, data: bytes) -> None:
-        headers, body = data.split(b"\r\n\r\n")
-
-        headers_raw = headers.split(b"\r\n")
-        self.version = self.heading[0]
-        if self.version not in self.SIPCompatibleVersions:
-            raise SIPParseError(f"SIP Version {self.version} not compatible.")
-
-        self.status = SIPStatus(int(self.heading[1]))
-
-        self.parse_raw_header(headers_raw, self.parse_header)
-
-        self.parse_raw_body(
-            body,
-            self.headers.get("Content-Type", "text/plain"),
-            self.parse_body,
-        )
-
-    def parse_sip_request(self, data: bytes) -> None:
-        headers, body = data.split(b"\r\n\r\n")
-
-        headers_raw = headers.split(b"\r\n")
-        self.version = self.heading[2]
-        if self.version not in self.SIPCompatibleVersions:
-            raise SIPParseError(f"SIP Version {self.version} not compatible.")
-
-        self.method = self.heading[0]
-        self.to = self.__get_uri_header(self.heading[1])
-
-        self.parse_raw_header(headers_raw, self.parse_header)
-
-        self.parse_raw_body(
-            body,
-            self.headers.get("Content-Type", "text/plain"),
-            self.parse_body,
-        )
diff --git a/pyVoIP/SIP/message/__init__.py b/pyVoIP/SIP/message/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyVoIP/SIP/message/message.py b/pyVoIP/SIP/message/message.py
new file mode 100644
index 0000000..52a2010
--- /dev/null
+++ b/pyVoIP/SIP/message/message.py
@@ -0,0 +1,198 @@
+from enum import Enum
+from pyVoIP import regex
+from pyVoIP.SIP.error import SIPParseError
+from pyVoIP.SIP.message.parse import (
+    parse_raw_headers,
+    parse_raw_body,
+    get_uri_header,
+)
+from pyVoIP.SIP.message.response_codes import ResponseCode
+from pyVoIP.types import URI_HEADER
+from typing import Any, Dict, List, Union
+import pyVoIP
+
+
+__all__ = ["SIPMethod", "SIPMessage", "SIPRequest", "SIPResponse"]
+
+
+debug = pyVoIP.debug
+
+
+class SIPMethod(Enum):
+    INVITE = "INVITE"
+    ACK = "ACK"
+    BYE = "BYE"
+    CANCEL = "CANCEL"
+    OPTIONS = "OPTIONS"
+    NOTIFY = "NOTIFY"
+    REGISTER = "REGISTER"
+    MESSAGE = "MESSAGE"
+    SUBSCRIBE = "SUBSCRIBE"
+    REFER = "REFER"
+
+    def __str__(self) -> str:
+        return self._value_
+
+    def __repr__(self) -> str:
+        return str(self)
+
+
+class SIPMessage:
+    def __init__(
+        self,
+        start_line: List[str],
+        headers: Dict[str, Any],
+        body: Dict[str, Any],
+        authentication: Dict[str, Union[str, List[str]]],
+        raw: bytes,
+    ):
+        self.start_line = start_line
+        self.headers = headers
+        self.body = body
+        self.authentication = authentication
+        self.raw = raw
+
+    def summary(self) -> str:
+        data = ""
+        data += f"{' '.join(self.start_line)}\n\n"
+        data += "Headers:\n"
+        for x in self.headers:
+            data += f"{x}: {self.headers[x]}\n"
+        data += "\n"
+        data += "Body:\n"
+        for x in self.body:
+            data += f"{x}: {self.body[x]}\n"
+        data += "\n"
+        data += "Raw:\n"
+        data += str(self.raw)
+
+        return data
+
+    @staticmethod
+    def from_bytes(data: bytes) -> Union["SIPRequest", "SIPResponse"]:
+        parsed_headers: Dict[str, Any] = {"Via": []}
+        parsed_body: Dict[str, Any] = {}
+        authentication: Dict[str, Union[str, List[str]]] = {}
+        version_match = regex.SIP_VERSION_MATCH
+
+        try:
+            try:
+                headers, body = data.split(b"\r\n\r\n")
+            except ValueError as ve:
+                debug(f"Error unpacking data, only using headers. ({ve})")
+                headers = data
+                body = b""
+
+            headers_raw = headers.split(b"\r\n")
+            start_line = str(headers_raw.pop(0), "utf8").split(" ")
+            check = start_line[0]
+
+            response = False
+
+            if version_match.match(check):
+                if check.upper() not in pyVoIP.SIPCompatibleVersions:
+                    raise SIPParseError(f"SIP Version {check} not compatible.")
+
+                response = True
+                status = ResponseCode(int(start_line[1]))
+            else:
+                if start_line[2].upper() not in pyVoIP.SIPCompatibleVersions:
+                    raise SIPParseError(
+                        f"SIP Version {start_line[2]} not compatible."
+                    )
+                if start_line[0] not in map(lambda x: str(x), list(SIPMethod)):
+                    raise SIPParseError(
+                        f"SIP Method `{start_line[0]}` not supported."
+                    )
+
+                method = SIPMethod(start_line[0])
+                destination = get_uri_header(start_line[1])
+
+            parsed_headers = parse_raw_headers(headers_raw)
+
+            authentication = {}
+            if "WWW-Authenticate" in parsed_headers:
+                authentication = parsed_headers["WWW-Authenticate"]
+            elif "Authorization" in parsed_headers:
+                authentication = parsed_headers["Authorization"]
+            elif "Proxy-Authenticate" in parsed_headers:
+                authentication = parsed_headers["Proxy-Authenticate"]
+
+            parsed_body = parse_raw_body(
+                body, parsed_headers.get("Content-Type", "text/plain")
+            )
+
+            if response:
+                return SIPResponse(
+                    start_line,
+                    parsed_headers,
+                    parsed_body,
+                    authentication,
+                    data,
+                    status,
+                )
+            return SIPRequest(
+                start_line,
+                parsed_headers,
+                parsed_body,
+                authentication,
+                data,
+                method,
+                destination,
+            )
+
+        except Exception as e:
+            if type(e) is not SIPParseError:
+                raise SIPParseError(e) from e
+            raise
+
+    @staticmethod
+    def from_string(data: str) -> Union["SIPRequest", "SIPResponse"]:
+        try:
+            return SIPMessage.from_bytes(data.encode("utf8"))
+        except Exception as e:
+            if type(e) is not SIPParseError:
+                raise SIPParseError(e) from e
+            raise
+
+
+class SIPRequest(SIPMessage):
+    def __init__(
+        self,
+        start_line: List[str],
+        headers: Dict[str, Any],
+        body: Dict[str, Any],
+        authentication: Dict[str, Union[str, List[str]]],
+        raw: bytes,
+        method: SIPMethod,
+        destination: URI_HEADER,
+    ):
+        super().__init__(start_line, headers, body, authentication, raw)
+        self.method = method
+        self.destination = destination
+
+    @property
+    def destination(self) -> URI_HEADER:
+        """
+        The destination property specifies the Request-URI in the Request-Line
+        detailed in RFC 3261 Section 7.1
+        """
+        return self._destination
+
+    @destination.setter
+    def destination(self, value: URI_HEADER) -> None:
+        self._destination = value
+
+
+class SIPResponse(SIPMessage):
+    def __init__(
+        self,
+        start_line: List[str],
+        headers: Dict[str, Any],
+        body: Dict[str, Any],
+        authentication: Dict[str, Union[str, List[str]]],
+        raw: bytes,
+        status: ResponseCode,
+    ):
+        super().__init__(start_line, headers, body, authentication, raw)
+        self.status = status
diff --git a/pyVoIP/SIP/message/parse.py b/pyVoIP/SIP/message/parse.py
new file mode 100644
index 0000000..6173e10
--- /dev/null
+++ b/pyVoIP/SIP/message/parse.py
@@ -0,0 +1,435 @@
+from typing import Any, Dict, List
+from pyVoIP import regex
+from pyVoIP.types import URI_HEADER
+from pyVoIP.SIP.error import SIPParseError
+import pyVoIP
+
+
+# Compacts defined in RFC 3261 Section 7.3.3 and 20
+COMPACT_KEY = {
+    "i": "Call-ID",
+    "m": "Contact",
+    "e": "Content-Encoding",
+    "l": "Content-Length",
+    "c": "Content-Type",
+    "f": "From",
+    "s": "Subject",
+    "k": "Supported",
+    "t": "To",
+    "v": "Via",
+}
+
+
+def parse_raw_headers(raw_headers: List[bytes]) -> Dict[str, Any]:
+    headers: Dict[str, Any] = {"Via": []}
+    # Only use first occurance of VIA header field;
+    # got second VIA from Kamailio running in DOCKER
+    # According to RFC 3261 these messages should be
+    # discarded in a response
+    for x in raw_headers:
+        i = str(x, "utf8").split(": ")
+        if i[0] == "Via":
+            headers["Via"].append(i[1])
+        if i[0] not in headers.keys():
+            headers[i[0]] = i[1]
+
+    parsed_headers: Dict[str, Any] = {}
+    for key, val in headers.items():
+        if key in COMPACT_KEY.keys():
+            key = COMPACT_KEY[key]
+
+        parsed_headers[key] = parse_header(key, val)
+    return parsed_headers
+
+
+def parse_raw_body(body: bytes, ctype: str) -> Dict[str, Any]:
+    if len(body) > 0:
+        if ctype == "application/sdp":
+            parsed_body: Dict[str, Any] = {}
+            body_raw = body.split(b"\r\n")
+            for x in body_raw:
+                i = str(x, "utf8").split("=")
+                if i != [""]:
+                    parse_sdp_tag(parsed_body, i[0], i[1])
+            return parsed_body
+        else:
+            return {"content": body}
+    return {"content": None}
+
+
+def get_uri_header(data: str) -> URI_HEADER:
+    info = data.split(";tag=")
+    tag = ""
+    if len(info) >= 2:
+        tag = info[1]
+    raw = data
+    reg = regex.TO_FROM_MATCH
+    direct = "@" not in data
+    if direct:
+        reg = regex.TO_FROM_DIRECT_MATCH
+    match = reg.match(data)
+    if match is None:
+        raise SIPParseError(
+            "Regex failed to match To/From.\n\n"
+            + "Please open a GitHub Issue at "
+            + "https://www.github.com/tayler6000/pyVoIP "
+            + "and include the following:\n\n"
+            + f"{data=} {type(match)=}"
+        )
+    matches = match.groupdict()
+    if direct:
+        matches["user"] = ""
+        matches["password"] = ""
+    uri = f'{matches["uri_type"]}:{matches["user"]}@{matches["host"]}'
+    if direct:
+        uri = f'{matches["uri_type"]}:{matches["host"]}'
+    if matches["port"]:
+        uri += matches["port"]
+    uri_type = matches["uri_type"]
+    user = matches["user"]
+    password = matches["password"].strip(":") if matches["password"] else ""
+    display_name = (
+        matches["display_name"].strip().strip('"')
+        if matches["display_name"]
+        else ""
+    )
+    host = matches["host"]
+    port = int(matches["port"].strip(":")) if matches["port"] else 5060
+
+    return {
+        "raw": raw,
+        "tag": tag,
+        "uri": uri,
+        "uri-type": uri_type,
+        "user": user,
+        "password": password,
+        "display-name": display_name,
+        "host": host,
+        "port": port,
+    }
+
+
+def parse_header(header: str, data: str) -> Any:
+    if header == "Via":
+        vias = []
+        for d in data:
+            info = regex.VIA_SPLIT.split(d)
+            _type = info[0]  # SIP Method
+            _address = info[1].split(":")  # Tuple: address, port
+            _ip = _address[0]
+
+            """
+            If no port is provided in via header assume default port.
+            Needs to be str. Check response build for better str creation
+            """
+            _port = int(info[1].split(":")[1]) if len(_address) > 1 else 5060
+            _via = {"type": _type, "address": (_ip, _port)}
+
+            """
+            Sets branch, maddr, ttl, received, and rport if defined
+            as per RFC 3261 20.7
+            """
+            for x in info[2:]:
+                if "=" in x:
+                    try:
+                        _via[x.split("=")[0]] = int(x.split("=")[1])
+                    except ValueError:
+                        _via[x.split("=")[0]] = x.split("=")[1]
+                else:
+                    _via[x] = None
+            vias.append(_via)
+        return vias
+    elif header in ["To", "From", "Contact", "Refer-To"]:
+        return get_uri_header(data)
+    elif header == "CSeq":
+        return {
+            "check": int(data.split(" ")[0]),
+            "method": data.split(" ")[1],
+        }
+    elif header in ["Allow", "Supported", "Require"]:
+        return data.split(", ")
+    elif header == "Call-ID":
+        return data
+    elif header in (
+        "WWW-Authenticate",
+        "Authorization",
+        "Proxy-Authenticate",
+    ):
+        method = data.split(" ")[0]
+        data = data.replace(f"{method} ", "")
+        auth_match = regex.AUTH_MATCH
+        row_data = auth_match.findall(data)
+        auth_data: Dict[str, Any] = {"header": header, "method": method}
+        for var, data in row_data:
+            if var == "userhash":
+                auth_data[var] = (
+                    False if data.strip('"').lower() == "false" else True
+                )
+                continue
+            if var == "qop":
+                authorized = data.strip('"').split(",")
+                for i, value in enumerate(authorized):
+                    authorized[i] = value.strip()
+                auth_data[var] = authorized
+                continue
+            auth_data[var] = data.strip('"')
+        return auth_data
+    elif header == "Target-Dialog":
+        # Target-Dialog (tdialog) is specified in RFC 4538
+        params = data.split(";")
+        td_data: Dict[str, Any] = {
+            "callid": params.pop(0)
+        }  # key is callid to be consitenent with RFC 4538 Section 7
+        for x in params:
+            y = x.split("=")
+            td_data[y[0]] = y[1]
+        return td_data
+    elif header == "Refer-Sub":
+        # Refer-Sub (norefersub) is specified in RFC 4488
+        params = data.split(";")
+        rs_data: Dict[str, Any] = {
+            "value": True if params.pop(0) == "true" else False
+        }  # BNF states extens are possible
+        for x in params:
+            y = x.split("=")
+            rs_data[y[0]] = y[1]
+        return rs_data
+    else:
+        try:
+            return int(data)
+        except ValueError:
+            return data
+
+
+def parse_sdp_tag(parsed_body: Dict[str, Any], field: str, data: str) -> Any:
+    # Referenced RFC 4566 July 2006
+    if field == "v":
+        # SDP 5.1 Version
+        parsed_body[field] = int(data)
+    elif field == "o":
+        # SDP 5.2 Origin
+        # o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address> # noqa: E501
+        d = data.split(" ")
+        parsed_body[field] = {
+            "username": d[0],
+            "id": d[1],
+            "version": d[2],
+            "network_type": d[3],
+            "address_type": d[4],
+            "address": d[5],
+        }
+    elif field == "s":
+        # SDP 5.3 Session Name
+        # s=<session name>
+        parsed_body[field] = data
+    elif field == "i":
+        # SDP 5.4 Session Information
+        # i=<session-description>
+        parsed_body[field] = data
+    elif field == "u":
+        # SDP 5.5 URI
+        # u=<uri>
+        parsed_body[field] = data
+    elif field == "e" or field == "p":
+        # SDP 5.6 Email Address and Phone Number of person
+        # responsible for the conference
+        # e=<email-address>
+        # p=<phone-number>
+        parsed_body[field] = data
+    elif field == "c":
+        # SDP 5.7 Connection Data
+        # c=<nettype> <addrtype> <connection-address>
+        if "c" not in parsed_body:
+            parsed_body["c"] = []
+        d = data.split(" ")
+        # TTL Data and Multicast addresses may be specified.
+        # For IPv4 its listed as addr/ttl/number of addresses.
+        # c=IN IP4 224.2.1.1/127/3 means:
+        # c=IN IP4 224.2.1.1/127
+        # c=IN IP4 224.2.1.2/127
+        # c=IN IP4 224.2.1.3/127
+        # With the TTL being 127.
+        # IPv6 does not support time to live so you will only see a '/'
+        # for multicast addresses.
+        if "/" in d[2]:
+            if d[1] == "IP6":
+                parsed_body[field].append(
+                    {
+                        "network_type": d[0],
+                        "address_type": d[1],
+                        "address": d[2].split("/")[0],
+                        "ttl": None,
+                        "address_count": int(d[2].split("/")[1]),
+                    }
+                )
+            else:
+                address_data = d[2].split("/")
+                if len(address_data) == 2:
+                    parsed_body[field].append(
+                        {
+                            "network_type": d[0],
+                            "address_type": d[1],
+                            "address": address_data[0],
+                            "ttl": int(address_data[1]),
+                            "address_count": 1,
+                        }
+                    )
+                else:
+                    parsed_body[field].append(
+                        {
+                            "network_type": d[0],
+                            "address_type": d[1],
+                            "address": address_data[0],
+                            "ttl": int(address_data[1]),
+                            "address_count": int(address_data[2]),
+                        }
+                    )
+        else:
+            parsed_body[field].append(
+                {
+                    "network_type": d[0],
+                    "address_type": d[1],
+                    "address": d[2],
+                    "ttl": None,
+                    "address_count": 1,
+                }
+            )
+    elif field == "b":
+        # SDP 5.8 Bandwidth
+        # b=<bwtype>:<bandwidth>
+        # A bwtype of CT means Conference Total between all medias
+        # and all devices in the conference.
+        # A bwtype of AS means Applicaton Specific total for this
+        # media and this device.
+        # The bandwidth is given in kilobits per second.
+        # As this was written in 2006, this could be Kibibits.
+        # TODO: Implement Bandwidth restrictions
+        d = data.split(":")
+        parsed_body[field] = {"type": d[0], "bandwidth": d[1]}
+    elif field == "t":
+        # SDP 5.9 Timing
+        # t=<start-time> <stop-time>
+        d = data.split(" ")
+        parsed_body[field] = {"start": d[0], "stop": d[1]}
+    elif field == "r":
+        # SDP 5.10 Repeat Times
+        # r=<repeat interval> <active duration> <offsets from start-time> # noqa: E501
+        d = data.split(" ")
+        parsed_body[field] = {
+            "repeat": d[0],
+            "duration": d[1],
+            "offset1": d[2],
+            "offset2": d[3],
+        }
+    elif field == "z":
+        # SDP 5.11 Time Zones
+        # z=<adjustment time> <offset> <adjustment time> <offset> ....
+        # Used for change in timezones such as day light savings time.
+        d = data.split()
+        amount = len(d) / 2
+        parsed_body[field] = {}
+        for x in range(int(amount)):
+            parsed_body[field]["adjustment-time" + str(x)] = d[x * 2]
+            parsed_body[field]["offset" + str(x)] = d[x * 2 + 1]
+    elif field == "k":
+        # SDP 5.12 Encryption Keys
+        # k=<method>
+        # k=<method>:<encryption key>
+        if ":" in data:
+            d = data.split(":")
+            parsed_body[field] = {"method": d[0], "key": d[1]}
+        else:
+            parsed_body[field] = {"method": data}
+    elif field == "m":
+        # SDP 5.14 Media Descriptions
+        # m=<media> <port>/<number of ports> <proto> <fmt> ...
+        # <port> should be even, and <port>+1 should be the RTCP port.
+        # <number of ports> should coinside with number of
+        # addresses in SDP 5.7 c=
+        if "m" not in parsed_body:
+            parsed_body["m"] = []
+        d = data.split(" ")
+
+        if "/" in d[1]:
+            ports_raw = d[1].split("/")
+            port = ports_raw[0]
+            count = int(ports_raw[1])
+        else:
+            port = d[1]
+            count = 1
+        methods = d[3:]
+
+        parsed_body["m"].append(
+            {
+                "type": d[0],
+                "port": int(port),
+                "port_count": count,
+                "protocol": pyVoIP.RTP.RTPProtocol(d[2]),
+                "methods": methods,
+                "attributes": {},
+            }
+        )
+        for x in parsed_body["m"][-1]["methods"]:
+            parsed_body["m"][-1]["attributes"][x] = {}
+    elif field == "a":
+        # SDP 5.13 Attributes & 6.0 SDP Attributes
+        # a=<attribute>
+        # a=<attribute>:<value>
+
+        if "a" not in parsed_body:
+            parsed_body["a"] = {}
+
+        if ":" in data:
+            d = data.split(":")
+            attribute = d[0]
+            value = d[1]
+        else:
+            attribute = data
+            value = None
+
+        if value is not None:
+            if attribute == "rtpmap":
+                # a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] # noqa: E501
+                v = regex.SDP_A_SPLIT.split(value)
+                for t in parsed_body["m"]:
+                    if v[0] in t["methods"]:
+                        index = int(parsed_body["m"].index(t))
+                        break
+                if len(v) == 4:
+                    encoding = v[3]
+                else:
+                    encoding = None
+
+                parsed_body["m"][index]["attributes"][v[0]]["rtpmap"] = {
+                    "id": v[0],
+                    "name": v[1],
+                    "frequency": v[2],
+                    "encoding": encoding,
+                }
+
+            elif attribute == "fmtp":
+                # a=fmtp:<format> <format specific parameters>
+                d = value.split(" ")
+                for t in parsed_body["m"]:
+                    if d[0] in t["methods"]:
+                        index = int(parsed_body["m"].index(t))
+                        break
+
+                parsed_body["m"][index]["attributes"][d[0]]["fmtp"] = {
+                    "id": d[0],
+                    "settings": d[1:],
+                }
+            else:
+                parsed_body["a"][attribute] = value
+        else:
+            if (
+                attribute == "recvonly"
+                or attribute == "sendrecv"
+                or attribute == "sendonly"
+                or attribute == "inactive"
+            ):
+                parsed_body["a"]["transmit_type"] = pyVoIP.RTP.TransmitType(
+                    attribute
+                )  # noqa: E501
+    else:
+        parsed_body[field] = data
diff --git a/pyVoIP/SIP/message/response_codes.py b/pyVoIP/SIP/message/response_codes.py
new file mode 100644
index 0000000..7f42387
--- /dev/null
+++ b/pyVoIP/SIP/message/response_codes.py
@@ -0,0 +1,270 @@
+from enum import Enum
+
+
+class ResponseCode(Enum):
+    def __new__(cls, value: int, phrase: str = "", description: str = ""):
+        obj = object.__new__(cls)
+        obj._value_ = value
+
+        obj.phrase = phrase
+        obj.description = description
+        return obj
+
+    def __int__(self) -> int:
+        return self._value_
+
+    def __str__(self) -> str:
+        return f"{self._value_} {self.phrase}"
+
+    def __repr__(self) -> str:
+        return str(self)
+
+    @property
+    def phrase(self) -> str:
+        return self._phrase
+
+    @phrase.setter
+    def phrase(self, value: str) -> None:
+        self._phrase = value
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @description.setter
+    def description(self, value: str) -> None:
+        self._description = value
+
+    # Informational
+    TRYING = (
+        100,
+        "Trying",
+        "Extended search being performed, may take a significant time",
+    )
+    RINGING = (
+        180,
+        "Ringing",
+        "Destination user agent received INVITE, "
+        + "and is alerting user of call",
+    )
+    FORWARDED = 181, "Call is Being Forwarded"
+    QUEUED = 182, "Queued"
+    SESSION_PROGRESS = 183, "Session Progress"
+    TERMINATED = 199, "Early Dialog Terminated"
+
+    # Success
+    OK = 200, "OK", "Request successful"
+    ACCEPTED = (
+        202,
+        "Accepted",
+        "Request accepted, processing continues (Deprecated.)",
+    )
+    NO_NOTIFICATION = (
+        204,
+        "No Notification",
+        "Request fulfilled, nothing follows",
+    )
+
+    # Redirection
+    MULTIPLE_CHOICES = (
+        300,
+        "Multiple Choices",
+        "Object has several resources -- see URI list",
+    )
+    MOVED_PERMANENTLY = (
+        301,
+        "Moved Permanently",
+        "Object moved permanently -- see URI list",
+    )
+    MOVED_TEMPORARILY = (
+        302,
+        "Moved Temporarily",
+        "Object moved temporarily -- see URI list",
+    )
+    USE_PROXY = (
+        305,
+        "Use Proxy",
+        "You must use proxy specified in Location to "
+        + "access this resource",
+    )
+    ALTERNATE_SERVICE = (
+        380,
+        "Alternate Service",
+        "The call failed, but alternatives are available -- see URI list",
+    )
+
+    # Client Error
+    BAD_REQUEST = (
+        400,
+        "Bad Request",
+        "Bad request syntax or unsupported method",
+    )
+    UNAUTHORIZED = (
+        401,
+        "Unauthorized",
+        "No permission -- see authorization schemes",
+    )
+    PAYMENT_REQUIRED = (
+        402,
+        "Payment Required",
+        "No payment -- see charging schemes",
+    )
+    FORBIDDEN = (
+        403,
+        "Forbidden",
+        "Request forbidden -- authorization will not help",
+    )
+    NOT_FOUND = (404, "Not Found", "Nothing matches the given URI")
+    METHOD_NOT_ALLOWED = (
+        405,
+        "Method Not Allowed",
+        "Specified method is invalid for this resource",
+    )
+    NOT_ACCEPTABLE = (
+        406,
+        "Not Acceptable",
+        "URI not available in preferred format",
+    )
+    PROXY_AUTHENTICATION_REQUIRED = (
+        407,
+        "Proxy Authentication Required",
+        "You must authenticate with this proxy before proceeding",
+    )
+    REQUEST_TIMEOUT = (
+        408,
+        "Request Timeout",
+        "Request timed out; try again later",
+    )
+    CONFLICT = 409, "Conflict", "Request conflict"
+    GONE = (
+        410,
+        "Gone",
+        "URI no longer exists and has been permanently removed",
+    )
+    LENGTH_REQUIRED = (
+        411,
+        "Length Required",
+        "Client must specify Content-Length",
+    )
+    CONDITIONAL_REQUEST_FAILED = 412, "Conditional Request Failed"
+    REQUEST_ENTITY_TOO_LARGE = (
+        413,
+        "Request Entity Too Large",
+        "Entity is too large",
+    )
+    REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long", "URI is too long"
+    UNSUPPORTED_MEDIA_TYPE = (
+        415,
+        "Unsupported Media Type",
+        "Entity body in unsupported format",
+    )
+    UNSUPPORTED_URI_SCHEME = (
+        416,
+        "Unsupported URI Scheme",
+        "Cannot satisfy request",
+    )
+    UNKOWN_RESOURCE_PRIORITY = (
+        417,
+        "Unkown Resource-Priority",
+        "There was a resource-priority option tag, "
+        + "but no Resource-Priority header",
+    )
+    BAD_EXTENSION = (
+        420,
+        "Bad Extension",
+        "Bad SIP Protocol Extension used, not understood by the server.",
+    )
+    EXTENSION_REQUIRED = (
+        421,
+        "Extension Required",
+        "Server requeires a specific extension to be "
+        + "listed in the Supported header.",
+    )
+    SESSION_INTERVAL_TOO_SMALL = 422, "Session Interval Too Small"
+    SESSION_INTERVAL_TOO_BRIEF = 423, "Session Interval Too Breif"
+    BAD_LOCATION_INFORMATION = 424, "Bad Location Information"
+    USE_IDENTITY_HEADER = (
+        428,
+        "Use Identity Header",
+        "The server requires an Identity header, "
+        + "and one has not been provided.",
+    )
+    PROVIDE_REFERRER_IDENTITY = 429, "Provide Referrer Identity"
+    """
+    This response is intended for use between proxy devices,
+    and should not be seen by an endpoint. If it is seen by one,
+    it should be treated as a 400 Bad Request response.
+    """
+    FLOW_FAILED = (
+        430,
+        "Flow Failed",
+        "A specific flow to a user agent has failed, "
+        + "although other flows may succeed.",
+    )
+    ANONYMITY_DISALLOWED = 433, "Anonymity Disallowed"
+    BAD_IDENTITY_INFO = 436, "Bad Identity-Info"
+    UNSUPPORTED_CERTIFICATE = 437, "Unsupported Certificate"
+    INVALID_IDENTITY_HEADER = 438, "Invalid Identity Header"
+    FIRST_HOP_LACKS_OUTBOUND_SUPPORT = 439, "First Hop Lacks Outbound Support"
+    MAX_BREADTH_EXCEEDED = 440, "Max-Breadth Exceeded"
+    BAD_INFO_PACKAGE = 469, "Bad Info Package"
+    CONSENT_NEEDED = 470, "Consent Needed"
+    TEMPORARILY_UNAVAILABLE = 480, "Temporarily Unavailable"
+    CALL_OR_TRANSACTION_DOESNT_EXIST = 481, "Call/Transaction Does Not Exist"
+    LOOP_DETECTED = 482, "Loop Detected"
+    TOO_MANY_HOPS = 483, "Too Many Hops"
+    ADDRESS_INCOMPLETE = 484, "Address Incomplete"
+    AMBIGUOUS = 485, "Ambiguous"
+    BUSY_HERE = 486, "Busy Here", "Callee is busy"
+    REQUEST_TERMINATED = 487, "Request Terminated"
+    NOT_ACCEPTABLE_HERE = 488, "Not Acceptable Here"
+    BAD_EVENT = 489, "Bad Event"
+    REQUEST_PENDING = 491, "Request Pending"
+    UNDECIPHERABLE = 493, "Undecipherable"
+    SECURITY_AGREEMENT_REQUIRED = 494, "Security Agreement Required"
+
+    # Server Errors
+    INTERNAL_SERVER_ERROR = (
+        500,
+        "Internal Server Error",
+        "Server got itself in trouble",
+    )
+    NOT_IMPLEMENTED = (
+        501,
+        "Not Implemented",
+        "Server does not support this operation",
+    )
+    BAD_GATEWAY = (
+        502,
+        "Bad Gateway",
+        "Invalid responses from another server/proxy",
+    )
+    SERVICE_UNAVAILABLE = (
+        503,
+        "Service Unavailable",
+        "The server cannot process the request due to a high load",
+    )
+    GATEWAY_TIMEOUT = (
+        504,
+        "Server Timeout",
+        "The server did not receive a timely response",
+    )
+    SIP_VERSION_NOT_SUPPORTED = (
+        505,
+        "SIP Version Not Supported",
+        "Cannot fulfill request",
+    )
+    MESSAGE_TOO_LONG = 513, "Message Too Long"
+    PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = (
+        555,
+        "Push Notification Service Not Supported",
+    )
+    PRECONDITION_FAILURE = 580, "Precondition Failure"
+
+    # Global Failure Responses
+    BUSY_EVERYWHERE = 600, "Busy Everywhere"
+    DECLINE = 603, "Decline"
+    DOES_NOT_EXIST_ANYWHERE = 604, "Does Not Exist Anywhere"
+    GLOBAL_NOT_ACCEPTABLE = 606, "Not Acceptable"
+    UNWANTED = 607, "Unwanted"
+    REJECTED = 608, "Rejected"
diff --git a/pyVoIP/VoIP/call.py b/pyVoIP/VoIP/call.py
index 89ba0dc..8d39890 100644
--- a/pyVoIP/VoIP/call.py
+++ b/pyVoIP/VoIP/call.py
@@ -1,7 +1,13 @@
 from enum import Enum
 from pyVoIP import RTP
 from pyVoIP.SIP.error import SIPParseError
-from pyVoIP.SIP.message import SIPMessage, SIPMessageType, SIPStatus
+from pyVoIP.SIP.message.message import (
+    SIPMessage,
+    SIPMethod,
+    SIPResponse,
+    SIPRequest,
+)
+from pyVoIP.SIP.message.response_codes import ResponseCode
 from pyVoIP.VoIP.error import InvalidStateError
 from threading import Lock, Timer
 from typing import Any, Dict, List, Optional, TYPE_CHECKING
@@ -89,21 +95,21 @@ def receiver(self):
             if data is None:
                 continue
             try:
-                message = SIPMessage(data)
+                message = SIPMessage.from_bytes(data)
             except SIPParseError:
                 continue
-            if message.type is SIPMessageType.RESPONSE:
-                if message.status is SIPStatus.OK:
+            if type(message) is SIPResponse:
+                if message.status is ResponseCode.OK:
                     if self.state in [
                         CallState.DIALING,
                         CallState.RINGING,
                         CallState.PROGRESS,
                     ]:
                         self.answered(message)
-                elif message.status == SIPStatus.NOT_FOUND:
+                elif message.status == ResponseCode.NOT_FOUND:
                     pass
             else:
-                if message.method == "BYE":
+                if message.method == SIPMethod.BYE:
                     self.bye(message)
 
     def init_outgoing_call(self, ms: Optional[Dict[int, RTP.PayloadType]]):
@@ -279,16 +285,22 @@ def answer(self) -> None:
         if self.state != CallState.RINGING:
             raise InvalidStateError("Call is not ringing")
         m = self.gen_ms()
-        message = self.sip.gen_answer(
+        data = self.sip.gen_answer(
             self.request, self.session_id, m, self.sendmode
         )
-        self.conn.send(message)
-        message = SIPMessage(self.conn.recv())
-        if message.method != "ACK":
+        self.conn.send(data)
+        message = SIPMessage.from_bytes(self.conn.recv())
+        if type(message) is SIPResponse:
+            debug(
+                f"Received Response to OK instead of ACK: {message.status}:\n\n"
+                + f"{message.summary()}",
+                f"Received Response to OK instead of ACK: {message.status}",
+            )
+        elif type(message) is SIPRequest and message.method != SIPMethod.ACK:
             debug(
-                f"Received Message to OK other than ACK: {message.method}:\n\n"
+                f"Received Request to OK other than ACK: {message.method}:\n\n"
                 + f"{message.summary()}",
-                f"Received Message to OK other than ACK: {message.method}",
+                f"Received Request to OK other than ACK: {message.method}",
             )
         self.state = CallState.ANSWERED
 
@@ -313,13 +325,15 @@ def transfer(
         new_dialog = True
         conn = self.sip.send(request)
         conn.send(request)
-        response = SIPMessage(conn.recv())
+        response = SIPMessage.from_bytes(conn.recv())
+        if type(response) is not SIPResponse:
+            return False
 
         if response.status not in [
-            SIPStatus.OK,
-            SIPStatus.TRYING,
-            SIPStatus.ACCEPTED,
-            SIPStatus.BAD_EXTENSION,
+            ResponseCode.OK,
+            ResponseCode.TRYING,
+            ResponseCode.ACCEPTED,
+            ResponseCode.BAD_EXTENSION,
         ]:
             # If we've not received any of these responses, the client likely
             # does not accept out of dialog REFER requests.
@@ -330,10 +344,12 @@ def transfer(
                 self.request, user, uri, blind, new_dialog=False
             )
             conn.send(request)
-            response = SIPMessage(conn.recv())
+            response = SIPMessage.from_bytes(conn.recv())
+            if type(response) is not SIPResponse:
+                return False
 
         norefersub = True
-        if blind and response.status == SIPStatus.BAD_EXTENSION:
+        if blind and response.status == ResponseCode.BAD_EXTENSION:
             # If the client does not support norefersub, resend without it.
             norefersub = False
             if new_dialog:
@@ -343,9 +359,11 @@ def transfer(
                 conn = self.sip.send(request)
             else:
                 conn.send(request)
-            response = SIPMessage(conn.recv())
+            response = SIPMessage.from_bytes(conn.recv())
+            if type(response) is not SIPResponse:
+                return False
 
-        if response.status not in [SIPStatus.OK, SIPStatus.ACCEPTED]:
+        if response.status not in [ResponseCode.OK, ResponseCode.ACCEPTED]:
             return False
         if blind:
             if norefersub:
@@ -353,17 +371,20 @@ def transfer(
                     conn.close()
                 self.hangup()
                 return True
-        response = SIPMessage(conn.recv())
-        while response.method == "NOTIFY" and response.body.get(
-            "content", b""
-        ) in [
-            b"",
-            b"SIP/2.0 100 Trying\r\n",
-            b"SIP/2.0 181 Ringing\r\n",
-        ]:
+        response = SIPMessage.from_bytes(conn.recv())
+        while (
+            type(response) is SIPRequest
+            and response.method == SIPMethod.NOTIFY
+            and response.body.get("content", b"")
+            in [
+                b"",
+                b"SIP/2.0 100 Trying\r\n",
+                b"SIP/2.0 181 Ringing\r\n",
+            ]
+        ):
             reply = self.sip.gen_ok(response)
             conn.send(reply)
-            response = SIPMessage(conn.recv())
+            response = SIPMessage.from_bytes(conn.recv())
         if response.body.get("content", b"") == b"SIP/2.0 200 OK\r\n":
             reply = self.sip.gen_ok(response)
             conn.send(reply)
@@ -478,7 +499,7 @@ def ringing(self, request: SIPMessage) -> None:
             self.request = request
 
     def busy(self, request: SIPMessage) -> None:
-        self.bye()
+        self.bye(request)
 
     def deny(self) -> None:
         if self.state != CallState.RINGING:
diff --git a/pyVoIP/VoIP/phone.py b/pyVoIP/VoIP/phone.py
index afc3fd5..56b82a5 100644
--- a/pyVoIP/VoIP/phone.py
+++ b/pyVoIP/VoIP/phone.py
@@ -1,7 +1,8 @@
 from pyVoIP import RTP
 from pyVoIP.credentials import CredentialsManager
 from pyVoIP.SIP.client import SIPClient
-from pyVoIP.SIP.message import SIPMessage, SIPMessageType, SIPStatus
+from pyVoIP.SIP.message.message import SIPMessage, SIPRequest, SIPResponse
+from pyVoIP.SIP.message.response_codes import ResponseCode
 from pyVoIP.networking.sock import VoIPConnection
 from pyVoIP.networking.transport import TransportMode
 from pyVoIP.types import KEY_PASSWORD
@@ -105,7 +106,7 @@ def callback(
         self, conn: VoIPConnection, request: SIPMessage
     ) -> Optional[str]:
         # debug("Callback: "+request.summary())
-        if request.type == SIPMessageType.REQUEST:
+        if type(request) is SIPRequest:
             # debug("This is a message")
             if request.method == "INVITE":
                 self._callback_MSG_Invite(conn, request)
@@ -113,20 +114,20 @@ def callback(
                 self._callback_MSG_Bye(request)
             elif request.method == "OPTIONS":
                 return self._callback_MSG_Options(request)
-        else:
-            if request.status == SIPStatus.OK:
+        elif type(request) is SIPResponse:
+            if request.status == ResponseCode.OK:
                 self._callback_RESP_OK(request)
-            elif request.status == SIPStatus.NOT_FOUND:
+            elif request.status == ResponseCode.NOT_FOUND:
                 self._callback_RESP_NotFound(request)
-            elif request.status == SIPStatus.SERVICE_UNAVAILABLE:
+            elif request.status == ResponseCode.SERVICE_UNAVAILABLE:
                 self._callback_RESP_Unavailable(request)
-            elif request.status == SIPStatus.RINGING:
+            elif request.status == ResponseCode.RINGING:
                 self._callback_RESP_Ringing(request)
-            elif request.status == SIPStatus.SESSION_PROGRESS:
+            elif request.status == ResponseCode.SESSION_PROGRESS:
                 self._callback_RESP_Progress(request)
-            elif request.status == SIPStatus.BUSY_HERE:
+            elif request.status == ResponseCode.BUSY_HERE:
                 self._callback_RESP_Busy(request)
-            elif request.status == SIPStatus.REQUEST_TERMINATED:
+            elif request.status == ResponseCode.REQUEST_TERMINATED:
                 self._callback_RESP_Terminated(request)
         return None
 
@@ -355,7 +356,10 @@ def message(
         self, number: str, body: str, ctype: str = "text/plain"
     ) -> bool:
         response = self.sip.message(number, body, ctype)
-        return response and response.status == SIPStatus.OK
+        return (
+            type(response) is SIPResponse
+            and response.status == ResponseCode.OK
+        )
 
     def request_port(self, blocking=True) -> int:
         ports_available = [
diff --git a/pyVoIP/__init__.py b/pyVoIP/__init__.py
index 03a9fa7..11a23bb 100644
--- a/pyVoIP/__init__.py
+++ b/pyVoIP/__init__.py
@@ -91,9 +91,10 @@ def debug(s, e=None):
 
 # noqa because import will fail if debug is not defined
 from pyVoIP.RTP import PayloadType  # noqa: E402
+from pyVoIP.SIP.message.message import SIPMethod  # noqa: E402
 
-SIPCompatibleMethods = ["INVITE", "ACK", "BYE", "CANCEL", "OPTIONS", "NOTIFY"]
 SIPCompatibleVersions = ["SIP/2.0"]
+SIPCompatibleMethods = list(map(lambda x: str(x), list(SIPMethod)))
 
 RTPCompatibleVersions = [2]
 RTPCompatibleCodecs = [PayloadType.PCMU, PayloadType.PCMA, PayloadType.EVENT]
diff --git a/pyVoIP/networking/sock.py b/pyVoIP/networking/sock.py
index cfa7dc2..b552403 100644
--- a/pyVoIP/networking/sock.py
+++ b/pyVoIP/networking/sock.py
@@ -1,6 +1,6 @@
 from typing import TYPE_CHECKING, List, Optional, Tuple, Union
 from pyVoIP.types import KEY_PASSWORD, SOCKETS
-from pyVoIP.SIP.message import SIPMessage, SIPMessageType
+from pyVoIP.SIP.message.message import SIPMessage, SIPRequest
 from pyVoIP.SIP.error import SIPParseError
 from pyVoIP.networking.nat import NAT, AddressType
 from pyVoIP.networking.transport import TransportMode
@@ -42,27 +42,27 @@ def __init__(
             self.message
         )
         self._peak_buffer: Optional[bytes] = None
-        if conn and message.type == SIPMessageType.REQUEST:
+        if conn and type(message) is SIPRequest:
             if self.sock.mode.tls_mode:
                 client_context = ssl.create_default_context()
                 client_context.check_hostname = pyVoIP.TLS_CHECK_HOSTNAME
                 client_context.verify_mode = pyVoIP.TLS_VERIFY_MODE
                 self.conn = client_context.wrap_socket(
-                    self.conn, server_hostname=message.to["host"]
+                    self.conn, server_hostname=message.destination["host"]
                 )
-            addr = (message.to["host"], message.to["port"])
+            addr = (message.destination["host"], message.destination["port"])
             self.conn.connect(addr)
 
     def send(self, data: Union[bytes, str]) -> None:
         if type(data) is str:
             data = data.encode("utf8")
         try:
-            msg = SIPMessage(data)
+            msg = SIPMessage.from_bytes(data)
         except SIPParseError:
             return
         if not self.conn:  # If UDP
-            if msg.type == SIPMessageType.REQUEST:
-                addr = (msg.to["host"], msg.to["port"])
+            if type(msg) is SIPRequest:
+                addr = (msg.destination["host"], msg.destination["port"])
             else:
                 addr = msg.headers["Via"][0]["address"]
             self.sock.s.sendto(data, addr)
@@ -95,7 +95,7 @@ def _tcp_tls_recv(self, nbytes: int, timeout=0, peak=False) -> bytes:
         while not msg and not self.sock.SD:
             data = self.conn.recv(nbytes)
             try:
-                msg = SIPMessage(data)
+                msg = SIPMessage.from_bytes(data)
             except SIPParseError as e:
                 br = self.sock.gen_bad_request(
                     connection=self, error=e, received=data
@@ -402,7 +402,7 @@ def _tcp_tls_run(self) -> None:
             debug(f"Received new {self.mode} connection from {addr}.")
             data = conn.recv(8192)
             try:
-                message = SIPMessage(data)
+                message = SIPMessage.from_bytes(data)
             except SIPParseError:
                 continue
             debug("\n\nReceived SIP Message:")
@@ -416,7 +416,7 @@ def _udp_run(self) -> None:
             except OSError:
                 continue
             try:
-                message = SIPMessage(data)
+                message = SIPMessage.from_bytes(data)
             except SIPParseError:
                 continue
             debug("\n\nReceived UDP Message:")
@@ -471,11 +471,11 @@ def send(self, data: bytes) -> VoIPConnection:
         Creates a new connection, sends the data, then returns the socket
         """
         if self.mode == TransportMode.UDP:
-            conn = VoIPConnection(self, None, SIPMessage(data))
+            conn = VoIPConnection(self, None, SIPMessage.from_bytes(data))
             self.__register_connection(conn)
             conn.send(data)
             return conn
         s = socket.socket(socket.AF_INET, self.mode.socket_type)
-        conn = VoIPConnection(self, s, SIPMessage(data))
+        conn = VoIPConnection(self, s, SIPMessage.from_bytes(data))
         conn.send(data)
         return conn
diff --git a/pyVoIP/regex.py b/pyVoIP/regex.py
index 3525e1e..9b8125a 100644
--- a/pyVoIP/regex.py
+++ b/pyVoIP/regex.py
@@ -20,3 +20,4 @@
     r'(?P<display_name>"?[\w ]*"? )?<?(?P<uri_type>sips?):(?P<host>[\w.]+)(?P<port>:[0-9]+)?>?'
 )
 SDP_A_SPLIT = re.compile(" |/")
+SIP_VERSION_MATCH = re.compile(r"(?:SIP|sip)/[0-9.]+")


Test patch:
diff --git a/tests/test_sip_requests.py b/tests/test_sip_requests.py
index 176b614..a2c7e6e 100644
--- a/tests/test_sip_requests.py
+++ b/tests/test_sip_requests.py
@@ -1,4 +1,4 @@
-from pyVoIP.SIP.message import SIPMessage
+from pyVoIP.SIP.message.message import SIPMessage, SIPRequest
 import pytest
 
 
@@ -459,7 +459,8 @@
     ],
 )
 def test_sip_headers(packet, expected):
-    message = SIPMessage(packet)
+    message = SIPMessage.from_bytes(packet)
+    assert type(message) is SIPRequest
     assert message.headers == expected
 
 
@@ -567,5 +568,6 @@ def test_sip_headers(packet, expected):
     ],
 )
 def test_sip_to(packet, expected):
-    message = SIPMessage(packet)
-    assert message.to == expected
+    message = SIPMessage.from_bytes(packet)
+    assert type(message) is SIPRequest
+    assert message.destination == expected
diff --git a/tests/test_sip_responses.py b/tests/test_sip_responses.py
index 53bde91..24a4fe2 100644
--- a/tests/test_sip_responses.py
+++ b/tests/test_sip_responses.py
@@ -1,4 +1,4 @@
-from pyVoIP.SIP.message import SIPMessage
+from pyVoIP.SIP.message.message import SIPMessage, SIPResponse
 import pytest
 
 
@@ -83,7 +83,8 @@
     ],
 )
 def test_sip_authentication(packet, expected):
-    message = SIPMessage(packet)
+    message = SIPMessage.from_bytes(packet)
+    assert type(message) is SIPResponse
     assert message.authentication == expected
 
 
@@ -206,7 +207,8 @@ def test_sip_authentication(packet, expected):
     ],
 )
 def test_sip_to_from(packet, expected):
-    message = SIPMessage(packet)
+    message = SIPMessage.from_bytes(packet)
+    assert type(message) is SIPResponse
     assert type(message.headers["To"]) == dict
     assert message.headers["To"] == expected
 
@@ -293,5 +295,6 @@ def test_sip_to_from(packet, expected):
     ],
 )
 def test_multi_via(packet, expected):
-    message = SIPMessage(packet)
+    message = SIPMessage.from_bytes(packet)
+    assert type(message) is SIPResponse
     assert message.headers["Via"] == expected